Completed
Pull Request — master (#1165)
by
unknown
02:38
created

GenericSparql::formatLimitAndOffset()   B

Complexity

Conditions 7
Paths 16

Size

Total Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
nc 16
nop 2
dl 0
loc 15
rs 8.8333
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * Generates SPARQL queries and provides access to the SPARQL endpoint.
5
 */
6
class GenericSparql {
7
    /**
8
     * A SPARQL Client eg. an EasyRDF instance.
9
     * @property EasyRdf\Sparql\Client $client
10
     */
11
    protected $client;
12
    /**
13
     * Graph uri.
14
     * @property object $graph
15
     */
16
    protected $graph;
17
    /**
18
     * A SPARQL query graph part template.
19
     * @property string $graphClause
20
     */
21
    protected $graphClause;
22
    /**
23
     * Model instance.
24
     * @property Model $model
25
     */
26
    protected $model;
27
28
    /**
29
     * Cache used to avoid expensive shorten() calls
30
     * @property array $qnamecache
31
     */
32
    private $qnamecache = array();
33
34
    /**
35
     * Requires the following three parameters.
36
     * @param string $endpoint SPARQL endpoint address.
37
     * @param string|null $graph Which graph to query: Either an URI, the special value "?graph"
38
     *                           to use the default graph, or NULL to not use a GRAPH clause.
39
     * @param object $model a Model instance.
40
     */
41
    public function __construct($endpoint, $graph, $model) {
42
        $this->graph = $graph;
43
        $this->model = $model;
44
45
        // create the EasyRDF SPARQL client instance to use
46
        $this->initializeHttpClient();
47
        $this->client = new EasyRdf\Sparql\Client($endpoint);
48
49
        // set graphClause so that it can be used by all queries
50
        if ($this->isDefaultEndpoint()) // default endpoint; query any graph (and catch it in a variable)
51
        {
52
            $this->graphClause = "GRAPH $graph";
53
        } elseif ($graph !== null) // query a specific graph
54
        {
55
            $this->graphClause = "GRAPH <$graph>";
56
        } else // query the default graph
57
        {
58
            $this->graphClause = "";
59
        }
60
61
    }
62
63
    /**
64
     * Returns prefix-definitions for a query
65
     *
66
     * @param string $query
67
     * @return string
68
    */
69
    protected function generateQueryPrefixes($query)
70
    {
71
        // Check for undefined prefixes
72
        $prefixes = '';
73
        foreach (EasyRdf\RdfNamespace::namespaces() as $prefix => $uri) {
74
            if (strpos($query, "{$prefix}:") !== false and
75
                strpos($query, "PREFIX {$prefix}:") === false
76
            ) {
77
                $prefixes .= "PREFIX {$prefix}: <{$uri}>\n";
78
            }
79
        }
80
        return $prefixes;
81
    }
82
83
    /**
84
     * Execute the SPARQL query using the SPARQL client, logging it as well.
85
     * @param string $query SPARQL query to perform
86
     * @return \EasyRdf\Sparql\Result|\EasyRdf\Graph query result
87
     */
88
    protected function query($query) {
89
        $queryId = sprintf("%05d", rand(0, 99999));
90
        $logger = $this->model->getLogger();
91
        $logger->info("[qid $queryId] SPARQL query:\n" . $this->generateQueryPrefixes($query) . "\n$query\n");
92
        $starttime = microtime(true);
93
        $result = $this->client->query($query);
94
        $elapsed = intval(round((microtime(true) - $starttime) * 1000));
95
        if(method_exists($result, 'numRows')) {
96
            $numRows = $result->numRows();
0 ignored issues
show
Bug introduced by
The method numRows does only exist in EasyRdf\Sparql\Result, but not in EasyRdf\Graph and EasyRdf\Http\Response.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
97
            $logger->info("[qid $queryId] result: $numRows rows returned in $elapsed ms");
98
        } else { // graph result
99
            $numTriples = $result->countTriples();
0 ignored issues
show
Bug introduced by
The method countTriples does only exist in EasyRdf\Graph, but not in EasyRdf\Http\Response and EasyRdf\Sparql\Result.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
100
            $logger->info("[qid $queryId] result: $numTriples triples returned in $elapsed ms");
101
        }
102
        return $result;
103
    }
104
105
106
    /**
107
     * Generates FROM clauses for the queries
108
     * @param Vocabulary[]|null $vocabs
109
     * @return string
110
     */
111
    protected function generateFromClause($vocabs=null) {
112
        $clause = '';
113
        if (!$vocabs) {
114
            return $this->graph !== '?graph' && $this->graph !== NULL ? "FROM <$this->graph>" : '';
115
        }
116
        $graphs = $this->getVocabGraphs($vocabs);
117
        foreach ($graphs as $graph) {
118
            $clause .= "FROM NAMED <$graph> ";
119
        }
120
        return $clause;
121
    }
122
    protected function initializeHttpClient() {
123
        // configure the HTTP client used by EasyRdf\Sparql\Client
124
        $httpclient = EasyRdf\Http::getDefaultHttpClient();
125
        $httpclient->setConfig(array('timeout' => $this->model->getConfig()->getSparqlTimeout()));
126
127
        // if special cache control (typically no-cache) was requested by the
128
        // client, set the same type of cache control headers also in subsequent
129
        // in the SPARQL requests (this is useful for performance testing)
130
        // @codeCoverageIgnoreStart
131
        $cacheControl = filter_input(INPUT_SERVER, 'HTTP_CACHE_CONTROL', FILTER_SANITIZE_STRING);
132
        $pragma = filter_input(INPUT_SERVER, 'HTTP_PRAGMA', FILTER_SANITIZE_STRING);
133
        if ($cacheControl !== null || $pragma !== null) {
134
            $val = $pragma !== null ? $pragma : $cacheControl;
135
            $httpclient->setHeaders('Cache-Control', $val);
136
        }
137
        // @codeCoverageIgnoreEnd
138
139
        EasyRdf\Http::setDefaultHttpClient($httpclient); // actually redundant..
140
    }
141
142
    /**
143
     * Return true if this is the default SPARQL endpoint, used as the facade to query
144
     * all vocabularies.
145
     */
146
147
    protected function isDefaultEndpoint() {
148
        return !is_null($this->graph) && $this->graph[0] == '?';
149
    }
150
151
    /**
152
     * Returns the graph instance
153
     * @return object EasyRDF graph instance.
154
     */
155
    public function getGraph() {
156
        return $this->graph;
157
    }
158
159
    /**
160
     * Shorten a URI
161
     * @param string $uri URI to shorten
162
     * @return string shortened URI, or original URI if it cannot be shortened
163
     */
164
    private function shortenUri($uri) {
165
        if (!array_key_exists($uri, $this->qnamecache)) {
166
            $res = new EasyRdf\Resource($uri);
167
            $qname = $res->shorten(); // returns null on failure
168
            $this->qnamecache[$uri] = ($qname !== null) ? $qname : $uri;
169
        }
170
        return $this->qnamecache[$uri];
171
    }
172
173
174
    /**
175
     * Generates the sparql query for retrieving concept and collection counts in a vocabulary.
176
     * @return string sparql query
177
     */
178
    private function generateCountConceptsQuery($array, $group) {
179
        $fcl = $this->generateFromClause();
180
        $optional = $array ? "UNION { ?type rdfs:subClassOf* <$array> }" : '';
181
        $optional .= $group ? "UNION { ?type rdfs:subClassOf* <$group> }" : '';
182
        $query = <<<EOQ
183
      SELECT (COUNT(?conc) as ?c) ?type ?typelabel $fcl WHERE {
184
        { ?conc a ?type .
185
        { ?type rdfs:subClassOf* skos:Concept . } UNION { ?type rdfs:subClassOf* skos:Collection . } $optional }
186
        OPTIONAL { ?type rdfs:label ?typelabel . }
187
      }
188
GROUP BY ?type ?typelabel
189
EOQ;
190
        return $query;
191
    }
192
193
    /**
194
     * Used for transforming the concept count query results.
195
     * @param EasyRdf\Sparql\Result $result query results to be transformed
196
     * @param string $lang language of labels
197
     * @return Array containing the label counts
198
     */
199
    private function transformCountConceptsResults($result, $lang) {
200
        $ret = array();
201
        foreach ($result as $row) {
202
            if (!isset($row->type)) {
203
                continue;
204
            }
205
            $ret[$row->type->getUri()]['type'] = $row->type->getUri();
206
            $ret[$row->type->getUri()]['count'] = $row->c->getValue();
207
            if (isset($row->typelabel) && $row->typelabel->getLang() === $lang) {
208
                $ret[$row->type->getUri()]['label'] = $row->typelabel->getValue();
209
            }
210
211
        }
212
        return $ret;
213
    }
214
215
    /**
216
     * Used for counting number of concepts and collections in a vocabulary.
217
     * @param string $lang language of labels
218
     * @return array with number of concepts in this vocabulary per label
219
     */
220
    public function countConcepts($lang = null, $array = null, $group = null) {
221
        $query = $this->generateCountConceptsQuery($array, $group);
222
        $result = $this->query($query);
223
        return $this->transformCountConceptsResults($result, $lang);
0 ignored issues
show
Bug introduced by
It seems like $result defined by $this->query($query) on line 222 can also be of type object<EasyRdf\Graph> or object<EasyRdf\Http\Response>; however, GenericSparql::transformCountConceptsResults() does only seem to accept object<EasyRdf\Sparql\Result>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
224
    }
225
226
    /**
227
     * @param array $langs Languages to query for
228
     * @param string[] $props property names
229
     * @return string sparql query
230
     */
231
    private function generateCountLangConceptsQuery($langs, $classes, $props) {
232
        $gcl = $this->graphClause;
233
        $classes = ($classes) ? $classes : array('http://www.w3.org/2004/02/skos/core#Concept');
234
235
	$quote_string = function($val) { return "'$val'"; };
236
	$quoted_values = array_map($quote_string, $langs);
237
	$langFilter = "FILTER(?lang IN (" . implode(',', $quoted_values) . "))";
238
239
        $values = $this->formatValues('?type', $classes, 'uri');
240
        $valuesProp = $this->formatValues('?prop', $props, null);
241
242
        $query = <<<EOQ
243
SELECT ?lang ?prop
244
  (COUNT(?label) as ?count)
245
WHERE {
246
  $gcl {
247
    $values
248
    $valuesProp
249
    ?conc a ?type .
250
    ?conc ?prop ?label .
251
    BIND(LANG(?label) AS ?lang)
252
    $langFilter
253
  }
254
}
255
GROUP BY ?lang ?prop ?type
256
EOQ;
257
        return $query;
258
    }
259
260
    /**
261
     * Transforms the CountLangConcepts results into an array of label counts.
262
     * @param EasyRdf\Sparql\Result $result query results to be transformed
263
     * @param array $langs Languages to query for
264
     * @param string[] $props property names
265
     */
266
    private function transformCountLangConceptsResults($result, $langs, $props) {
267
        $ret = array();
268
        // set default count to zero; overridden below if query found labels
269
        foreach ($langs as $lang) {
270
            foreach ($props as $prop) {
271
                $ret[$lang][$prop] = 0;
272
            }
273
        }
274
        foreach ($result as $row) {
275
            if (isset($row->lang) && isset($row->prop) && isset($row->count)) {
276
                $ret[$row->lang->getValue()][$row->prop->shorten()] +=
277
                $row->count->getValue();
278
            }
279
280
        }
281
        ksort($ret);
282
        return $ret;
283
    }
284
285
    /**
286
     * Counts the number of concepts in a easyRDF graph with a specific language.
287
     * @param array $langs Languages to query for
288
     * @return Array containing count of concepts for each language and property.
289
     */
290
    public function countLangConcepts($langs, $classes = null) {
291
        $props = array('skos:prefLabel', 'skos:altLabel', 'skos:hiddenLabel');
292
        $query = $this->generateCountLangConceptsQuery($langs, $classes, $props);
293
        // Count the number of terms in each language
294
        $result = $this->query($query);
295
        return $this->transformCountLangConceptsResults($result, $langs, $props);
0 ignored issues
show
Bug introduced by
It seems like $result defined by $this->query($query) on line 294 can also be of type object<EasyRdf\Graph> or object<EasyRdf\Http\Response>; however, GenericSparql::transformCountLangConceptsResults() does only seem to accept object<EasyRdf\Sparql\Result>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
296
    }
297
298
    /**
299
     * Formats a VALUES clause (SPARQL 1.1) which states that the variable should be bound to one
300
     * of the constants given.
301
     * @param string $varname variable name, e.g. "?uri"
302
     * @param array $values the values
303
     * @param string $type type of values: "uri", "literal" or null (determines quoting style)
304
     */
305
    protected function formatValues($varname, $values, $type = null) {
306
        $constants = array();
307
        foreach ($values as $val) {
308
            if ($type == 'uri') {
309
                $val = "<$val>";
310
            }
311
312
            if ($type == 'literal') {
313
                $val = "'$val'";
314
            }
315
316
            $constants[] = "($val)";
317
        }
318
        $values = implode(" ", $constants);
319
320
        return "VALUES ($varname) { $values }";
321
    }
322
323
    /**
324
     * Filters multiple instances of the same vocabulary from the input array.
325
     * @param \Vocabulary[]|null $vocabs array of Vocabulary objects
326
     * @return \Vocabulary[]
327
     */
328
    private function filterDuplicateVocabs($vocabs) {
329
        // filtering duplicates
330
        $uniqueVocabs = array();
331
        if ($vocabs !== null && sizeof($vocabs) > 0) {
332
            foreach ($vocabs as $voc) {
333
                $uniqueVocabs[$voc->getId()] = $voc;
334
            }
335
        }
336
337
        return $uniqueVocabs;
338
    }
339
340
    /**
341
     * Generates a sparql query for one or more concept URIs
342
     * @param mixed $uris concept URI (string) or array of URIs
343
     * @param string|null $arrayClass the URI for thesaurus array class, or null if not used
344
     * @param \Vocabulary[]|null $vocabs array of Vocabulary objects
345
     * @return string sparql query
346
     */
347
    private function generateConceptInfoQuery($uris, $arrayClass, $vocabs) {
348
        $gcl = $this->graphClause;
349
        $fcl = empty($vocabs) ? '' : $this->generateFromClause($vocabs);
350
        $values = $this->formatValues('?uri', $uris, 'uri');
351
        $uniqueVocabs = $this->filterDuplicateVocabs($vocabs);
352
        $valuesGraph = empty($vocabs) ? $this->formatValuesGraph($uniqueVocabs) : '';
353
354
        if ($arrayClass === null) {
355
            $construct = $optional = "";
356
        } else {
357
            // add information that can be used to format narrower concepts by
358
            // the array they belong to ("milk by source animal" use case)
359
            $construct = "\n ?x skos:member ?o . ?x skos:prefLabel ?xl . ?x a <$arrayClass> .";
360
            $optional = "\n OPTIONAL {
361
                      ?x skos:member ?o .
362
                      ?x a <$arrayClass> .
363
                      ?x skos:prefLabel ?xl .
364
                      FILTER NOT EXISTS {
365
                        ?x skos:member ?other .
366
                        MINUS { ?other skos:broader ?uri }
367
                      }
368
                    }";
369
        }
370
        $query = <<<EOQ
371
CONSTRUCT {
372
 ?s ?p ?uri .
373
 ?sp ?uri ?op .
374
 ?uri ?p ?o .
375
 ?p rdfs:label ?proplabel .
376
 ?p rdfs:comment ?propcomm .
377
 ?p skos:definition ?propdef .
378
 ?p rdfs:subPropertyOf ?pp .
379
 ?pp rdfs:label ?plabel .
380
 ?o a ?ot .
381
 ?o skos:prefLabel ?opl .
382
 ?o rdfs:label ?ol .
383
 ?o rdf:value ?ov .
384
 ?o skos:notation ?on .
385
 ?o ?oprop ?oval .
386
 ?o ?xlprop ?xlval .
387
 ?directgroup skos:member ?uri .
388
 ?parent skos:member ?group .
389
 ?group skos:prefLabel ?grouplabel .
390
 ?b1 rdf:first ?item .
391
 ?b1 rdf:rest ?b2 .
392
 ?item a ?it .
393
 ?item skos:prefLabel ?il .
394
 ?group a ?grouptype . $construct
395
} $fcl WHERE {
396
 $values
397
 $gcl {
398
  {
399
    ?s ?p ?uri .
400
    FILTER(!isBlank(?s))
401
    FILTER(?p != skos:inScheme)
402
    FILTER NOT EXISTS { ?s owl:deprecated true . }
403
  }
404
  UNION
405
  { ?sp ?uri ?op . }
406
  UNION
407
  {
408
    ?directgroup skos:member ?uri .
409
    ?group skos:member+ ?uri .
410
    ?group skos:prefLabel ?grouplabel .
411
    ?group a ?grouptype .
412
    OPTIONAL { ?parent skos:member ?group }
413
  }
414
  UNION
415
  {
416
   ?uri ?p ?o .
417
   OPTIONAL {
418
     ?o rdf:rest* ?b1 .
419
     ?b1 rdf:first ?item .
420
     ?b1 rdf:rest ?b2 .
421
     OPTIONAL { ?item a ?it . }
422
     OPTIONAL { ?item skos:prefLabel ?il . }
423
   }
424
   OPTIONAL {
425
     { ?p rdfs:label ?proplabel . }
426
     UNION
427
     { ?p rdfs:comment ?propcomm . }
428
     UNION
429
     { ?p skos:definition ?propdef . }
430
     UNION
431
     { ?p rdfs:subPropertyOf ?pp . }
432
   }
433
   OPTIONAL {
434
     { ?o a ?ot . }
435
     UNION
436
     { ?o skos:prefLabel ?opl . }
437
     UNION
438
     { ?o rdfs:label ?ol . }
439
     UNION
440
     { ?o rdf:value ?ov . 
441
       OPTIONAL { ?o ?oprop ?oval . }
442
     }
443
     UNION
444
     { ?o skos:notation ?on . }
445
     UNION
446
     { ?o a skosxl:Label .
447
       ?o ?xlprop ?xlval }
448
   } $optional
449
  }
450
 }
451
}
452
$valuesGraph
453
EOQ;
454
        return $query;
455
    }
456
457
    /**
458
     * Transforms ConceptInfo query results into an array of Concept objects
459
     * @param EasyRdf\Graph $result query results to be transformed
460
     * @param array $uris concept URIs
461
     * @param \Vocabulary[] $vocabs array of Vocabulary object
462
     * @param string|null $clang content language
463
     * @return Concept[] array of Concept objects
464
     */
465
    private function transformConceptInfoResults($result, $uris, $vocabs, $clang) {
466
        $conceptArray = array();
467
        foreach ($uris as $index => $uri) {
468
            $conc = $result->resource($uri);
469
            if (is_array($vocabs)) {
470
                $vocab = (sizeof($vocabs) == 1) ? $vocabs[0] : $vocabs[$index];
471
            } else {
472
                $vocab = null;
473
            }
474
            $conceptArray[] = new Concept($this->model, $vocab, $conc, $result, $clang);
475
        }
476
        return $conceptArray;
477
    }
478
479
    /**
480
     * Returns information (as a graph) for one or more concept URIs
481
     * @param mixed $uris concept URI (string) or array of URIs
482
     * @param string|null $arrayClass the URI for thesaurus array class, or null if not used
483
     * @param \Vocabulary[]|null $vocabs vocabularies to target
484
     * @return \EasyRdf\Graph
485
     */
486
    public function queryConceptInfoGraph($uris, $arrayClass = null, $vocabs = array()) {
487
        // if just a single URI is given, put it in an array regardless
488
        if (!is_array($uris)) {
489
            $uris = array($uris);
490
        }
491
492
        $query = $this->generateConceptInfoQuery($uris, $arrayClass, $vocabs);
493
        $result = $this->query($query);
494
        return $result;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $result; (EasyRdf\Http\Response|Ze...ql\Result|EasyRdf\Graph) is incompatible with the return type documented by GenericSparql::queryConceptInfoGraph of type EasyRdf\Graph.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
495
    }
496
497
    /**
498
     * Returns information (as an array of Concept objects) for one or more concept URIs
499
     * @param mixed $uris concept URI (string) or array of URIs
500
     * @param string|null $arrayClass the URI for thesaurus array class, or null if not used
501
     * @param \Vocabulary[] $vocabs vocabularies to target
502
     * @param string|null $clang content language
503
     * @return Concept[]
504
     */
505
    public function queryConceptInfo($uris, $arrayClass = null, $vocabs = array(), $clang = null) {
506
        // if just a single URI is given, put it in an array regardless
507
        if (!is_array($uris)) {
508
            $uris = array($uris);
509
        }
510
        $result = $this->queryConceptInfoGraph($uris, $arrayClass, $vocabs);
511
        if ($result->isEmpty()) {
512
            return [];
513
        }
514
        return $this->transformConceptInfoResults($result, $uris, $vocabs, $clang);
515
    }
516
517
    /**
518
     * Generates the sparql query for queryTypes
519
     * @param string $lang
520
     * @return string sparql query
521
     */
522
    private function generateQueryTypesQuery($lang) {
523
        $fcl = $this->generateFromClause();
524
        $query = <<<EOQ
525
SELECT DISTINCT ?type ?label ?superclass $fcl
526
WHERE {
527
  {
528
    { BIND( skos:Concept as ?type ) }
529
    UNION
530
    { BIND( skos:Collection as ?type ) }
531
    UNION
532
    { BIND( isothes:ConceptGroup as ?type ) }
533
    UNION
534
    { BIND( isothes:ThesaurusArray as ?type ) }
535
    UNION
536
    { ?type rdfs:subClassOf/rdfs:subClassOf* skos:Concept . }
537
    UNION
538
    { ?type rdfs:subClassOf/rdfs:subClassOf* skos:Collection . }
539
  }
540
  OPTIONAL {
541
    ?type rdfs:label ?label .
542
    FILTER(langMatches(lang(?label), '$lang'))
543
  }
544
  OPTIONAL {
545
    ?type rdfs:subClassOf ?superclass .
546
  }
547
  FILTER EXISTS {
548
    ?s a ?type .
549
    ?s skos:prefLabel ?prefLabel .
550
  }
551
}
552
EOQ;
553
        return $query;
554
    }
555
556
    /**
557
     * Transforms the results into an array format.
558
     * @param EasyRdf\Sparql\Result $result
559
     * @return array Array with URIs (string) as key and array of (label, superclassURI) as value
560
     */
561
    private function transformQueryTypesResults($result) {
562
        $ret = array();
563
        foreach ($result as $row) {
564
            $type = array();
565
            if (isset($row->label)) {
566
                $type['label'] = $row->label->getValue();
567
            }
568
569
            if (isset($row->superclass)) {
570
                $type['superclass'] = $row->superclass->getUri();
571
            }
572
573
            $ret[$row->type->getURI()] = $type;
574
        }
575
        return $ret;
576
    }
577
578
    /**
579
     * Retrieve information about types from the endpoint
580
     * @param string $lang
581
     * @return array Array with URIs (string) as key and array of (label, superclassURI) as value
582
     */
583
    public function queryTypes($lang) {
584
        $query = $this->generateQueryTypesQuery($lang);
585
        $result = $this->query($query);
586
        return $this->transformQueryTypesResults($result);
0 ignored issues
show
Bug introduced by
It seems like $result defined by $this->query($query) on line 585 can also be of type object<EasyRdf\Graph> or object<EasyRdf\Http\Response>; however, GenericSparql::transformQueryTypesResults() does only seem to accept object<EasyRdf\Sparql\Result>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
587
    }
588
589
    /**
590
     * Generates the concept scheme query.
591
     * @param string $conceptscheme concept scheme URI
592
     * @return string sparql query
593
     */
594
    private function generateQueryConceptSchemeQuery($conceptscheme) {
595
        $fcl = $this->generateFromClause();
596
        $query = <<<EOQ
597
CONSTRUCT {
598
  <$conceptscheme> ?property ?value .
599
} $fcl WHERE {
600
  <$conceptscheme> ?property ?value .
601
  FILTER (?property != skos:hasTopConcept)
602
}
603
EOQ;
604
        return $query;
605
    }
606
607
    /**
608
     * Retrieves conceptScheme information from the endpoint.
609
     * @param string $conceptscheme concept scheme URI
610
     * @return \EasyRdf\Sparql\Result|\EasyRdf\Graph query result graph
611
     */
612
    public function queryConceptScheme($conceptscheme) {
613
        $query = $this->generateQueryConceptSchemeQuery($conceptscheme);
614
        return $this->query($query);
615
    }
616
617
    /**
618
     * Generates the queryConceptSchemes sparql query.
619
     * @param string $lang language of labels
620
     * @return string sparql query
621
     */
622
    private function generateQueryConceptSchemesQuery($lang) {
623
        $fcl = $this->generateFromClause();
624
        $query = <<<EOQ
625
SELECT ?cs ?label ?preflabel ?title ?domain ?domainLabel $fcl
626
WHERE {
627
 ?cs a skos:ConceptScheme .
628
 OPTIONAL{
629
    ?cs dcterms:subject ?domain.
630
    ?domain skos:prefLabel ?domainLabel.
631
    FILTER(langMatches(lang(?domainLabel), '$lang'))
632
}
633
 OPTIONAL {
634
   ?cs rdfs:label ?label .
635
   FILTER(langMatches(lang(?label), '$lang'))
636
 }
637
 OPTIONAL {
638
   ?cs skos:prefLabel ?preflabel .
639
   FILTER(langMatches(lang(?preflabel), '$lang'))
640
 }
641
 OPTIONAL {
642
   { ?cs dc11:title ?title }
643
   UNION
644
   { ?cs dc:title ?title }
645
   FILTER(langMatches(lang(?title), '$lang'))
646
 }
647
} 
648
ORDER BY ?cs
649
EOQ;
650
        return $query;
651
    }
652
653
    /**
654
     * Transforms the queryConceptScheme results into an array format.
655
     * @param EasyRdf\Sparql\Result $result
656
     * @return array
657
     */
658
    private function transformQueryConceptSchemesResults($result) {
659
        $ret = array();
660
        foreach ($result as $row) {
661
            $conceptscheme = array();
662
            if (isset($row->label)) {
663
                $conceptscheme['label'] = $row->label->getValue();
664
            }
665
666
            if (isset($row->preflabel)) {
667
                $conceptscheme['prefLabel'] = $row->preflabel->getValue();
668
            }
669
670
            if (isset($row->title)) {
671
                $conceptscheme['title'] = $row->title->getValue();
672
            }
673
            // add dct:subject and their labels in the result
674
            if(isset($row->domain) && isset($row->domainLabel)){
675
                $conceptscheme['subject']['uri']=$row->domain->getURI();
676
                $conceptscheme['subject']['prefLabel']=$row->domainLabel->getValue();
677
            }
678
679
            $ret[$row->cs->getURI()] = $conceptscheme;
680
        }
681
        return $ret;
682
    }
683
684
    /**
685
     * return a list of skos:ConceptScheme instances in the given graph
686
     * @param string $lang language of labels
687
     * @return array Array with concept scheme URIs (string) as keys and labels (string) as values
688
     */
689
    public function queryConceptSchemes($lang) {
690
        $query = $this->generateQueryConceptSchemesQuery($lang);
691
        $result = $this->query($query);
692
        return $this->transformQueryConceptSchemesResults($result);
0 ignored issues
show
Bug introduced by
It seems like $result defined by $this->query($query) on line 691 can also be of type object<EasyRdf\Graph> or object<EasyRdf\Http\Response>; however, GenericSparql::transform...ConceptSchemesResults() does only seem to accept object<EasyRdf\Sparql\Result>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
693
    }
694
695
    /**
696
     * Generate a VALUES clause for limiting the targeted graphs.
697
     * @param Vocabulary[]|null $vocabs the vocabularies to target
698
     * @return string[] array of graph URIs
699
     */
700
    protected function getVocabGraphs($vocabs) {
701
        if ($vocabs === null || sizeof($vocabs) == 0) {
702
            // searching from all vocabularies - limit to known graphs
703
            $vocabs = $this->model->getVocabularies();
704
        }
705
        $graphs = array();
706
        foreach ($vocabs as $voc) {
707
            $graph = $voc->getGraph();
708
            if (!is_null($graph) && !in_array($graph, $graphs)) {
709
                $graphs[] = $graph;
710
            }
711
        }
712
        return $graphs;
713
    }
714
715
    /**
716
     * Generate a VALUES clause for limiting the targeted graphs.
717
     * @param Vocabulary[]|null $vocabs array of Vocabulary objects to target
718
     * @return string VALUES clause, or "" if not necessary to limit
719
     */
720
    protected function formatValuesGraph($vocabs) {
721
        if (!$this->isDefaultEndpoint()) {
722
            return "";
723
        }
724
        $graphs = $this->getVocabGraphs($vocabs);
725
        return $this->formatValues('?graph', $graphs, 'uri');
726
    }
727
728
    /**
729
     * Generate a FILTER clause for limiting the targeted graphs.
730
     * @param array $vocabs array of Vocabulary objects to target
731
     * @return string FILTER clause, or "" if not necessary to limit
732
     */
733
    protected function formatFilterGraph($vocabs) {
734
        if (!$this->isDefaultEndpoint()) {
735
            return "";
736
        }
737
        $graphs = $this->getVocabGraphs($vocabs);
738
        $values = array();
739
        foreach ($graphs as $graph) {
740
          $values[] = "<$graph>";
741
        }
742
        if (count($values)) {
743
          return "FILTER (?graph IN (" . implode(',', $values) . "))";
744
        }
745
    }
746
747
    /**
748
     * Formats combined limit and offset clauses for the sparql query
749
     * @param int $limit maximum number of hits to retrieve; 0 for unlimited
750
     * @param int $offset offset of results to retrieve; 0 for beginning of list
751
     * @return string sparql query clauses
752
     */
753
    protected function formatLimitAndOffset($limit, $offset) {
754
        $limit = ($limit) ? 'LIMIT ' . $limit : '';
755
        $offset = ($offset) ? 'OFFSET ' . $offset : '';
756
        // eliminating whitespace and line changes when the conditions aren't needed.
757
        $limitandoffset = '';
758
        if ($limit && $offset) {
759
            $limitandoffset = "\n" . $limit . "\n" . $offset;
760
        } elseif ($limit) {
761
            $limitandoffset = "\n" . $limit;
762
        } elseif ($offset) {
763
            $limitandoffset = "\n" . $offset;
764
        }
765
766
        return $limitandoffset;
767
    }
768
769
    /**
770
     * Formats a sparql query clause for limiting the search to specific concept types.
771
     * @param array $types limit search to concepts of the given type(s)
772
     * @return string sparql query clause
773
     */
774
    protected function formatTypes($types) {
775
        $typePatterns = array();
776
        if (!empty($types)) {
777
            foreach ($types as $type) {
778
                $unprefixed = EasyRdf\RdfNamespace::expand($type);
779
                $typePatterns[] = "{ ?s a <$unprefixed> }";
780
            }
781
        }
782
783
        return implode(' UNION ', $typePatterns);
784
    }
785
786
    /**
787
     * @param string $prop property to include in the result eg. 'broader' or 'narrower'
788
     * @return string sparql query clause
789
     */
790
    private function formatPropertyCsvClause($prop) {
791
        # This expression creates a CSV row containing pairs of (uri,prefLabel) values.
792
        # The REPLACE is performed for quotes (" -> "") so they don't break the CSV format.
793
        $clause = <<<EOV
794
(GROUP_CONCAT(DISTINCT CONCAT(
795
 '"', IF(isIRI(?$prop),STR(?$prop),''), '"', ',',
796
 '"', REPLACE(IF(BOUND(?{$prop}lab),?{$prop}lab,''), '"', '""'), '"', ',',
797
 '"', REPLACE(IF(isLiteral(?{$prop}),?{$prop},''), '"', '""'), '"'
798
); separator='\\n') as ?{$prop}s)
799
EOV;
800
        return $clause;
801
    }
802
803
    /**
804
     * @return string sparql query clause
805
     */
806
    private function formatPrefLabelCsvClause() {
807
        # This expression creates a CSV row containing pairs of (prefLabel, lang) values.
808
        # The REPLACE is performed for quotes (" -> "") so they don't break the CSV format.
809
        $clause = <<<EOV
810
(GROUP_CONCAT(DISTINCT CONCAT(
811
 '"', STR(?pref), '"', ',', '"', lang(?pref), '"'
812
); separator='\\n') as ?preflabels)
813
EOV;
814
        return $clause;
815
    }
816
817
    /**
818
     * @param string $lang language code of the returned labels
819
     * @param array|null $fields extra fields to include in the result (array of strings). (default: null = none)
820
     * @return array sparql query clause
821
     */
822
    protected function formatExtraFields($lang, $fields) {
823
        // extra variable expressions to request and extra fields to query for
824
        $ret = array('extravars' => '', 'extrafields' => '');
825
826
        if ($fields === null) {
827
            return $ret;
828
        }
829
830
        if (in_array('prefLabel', $fields)) {
831
            $ret['extravars'] .= $this->formatPreflabelCsvClause();
832
            $ret['extrafields'] .= <<<EOF
833
OPTIONAL {
834
  ?s skos:prefLabel ?pref .
835
}
836
EOF;
837
            // removing the prefLabel from the fields since it has been handled separately
838
            $fields = array_diff($fields, array('prefLabel'));
839
        }
840
841
        foreach ($fields as $field) {
842
            $ret['extravars'] .= $this->formatPropertyCsvClause($field);
843
            $ret['extrafields'] .= <<<EOF
844
OPTIONAL {
845
  ?s skos:$field ?$field .
846
  FILTER(!isLiteral(?$field)||langMatches(lang(?{$field}), '$lang'))
847
  OPTIONAL { ?$field skos:prefLabel ?{$field}lab . FILTER(langMatches(lang(?{$field}lab), '$lang')) }
848
}
849
EOF;
850
        }
851
852
        return $ret;
853
    }
854
855
    /**
856
     * Generate condition for matching labels in SPARQL
857
     * @param string $term search term
858
     * @param string $searchLang language code used for matching labels (null means any language)
859
     * @return string sparql query snippet
860
     */
861
    protected function generateConceptSearchQueryCondition($term, $searchLang)
862
    {
863
        # use appropriate matching function depending on query type: =, strstarts, strends or full regex
864
        if (preg_match('/^[^\*]+$/', $term)) { // exact query
865
            $term = str_replace('\\', '\\\\', $term); // quote slashes
866
            $term = str_replace('\'', '\\\'', mb_strtolower($term, 'UTF-8')); // make lowercase and escape single quotes
867
            $filtercond = "LCASE(STR(?match)) = '$term'";
868
        } elseif (preg_match('/^[^\*]+\*$/', $term)) { // prefix query
869
            $term = substr($term, 0, -1); // remove the final asterisk
870
            $term = str_replace('\\', '\\\\', $term); // quote slashes
871
            $term = str_replace('\'', '\\\'', mb_strtolower($term, 'UTF-8')); // make lowercase and escape single quotes
872
            $filtercond = "STRSTARTS(LCASE(STR(?match)), '$term')";
873
        } elseif (preg_match('/^\*[^\*]+$/', $term)) { // suffix query
874
            $term = substr($term, 1); // remove the preceding asterisk
875
            $term = str_replace('\\', '\\\\', $term); // quote slashes
876
            $term = str_replace('\'', '\\\'', mb_strtolower($term, 'UTF-8')); // make lowercase and escape single quotes
877
            $filtercond = "STRENDS(LCASE(STR(?match)), '$term')";
878
        } else { // too complicated - have to use a regex
879
            # make sure regex metacharacters are not passed through
880
            $term = str_replace('\\', '\\\\', preg_quote($term));
881
            $term = str_replace('\\\\*', '.*', $term); // convert asterisk to regex syntax
882
            $term = str_replace('\'', '\\\'', $term); // ensure single quotes are quoted
883
            $filtercond = "REGEX(STR(?match), '^$term$', 'i')";
884
        }
885
886
        $labelcondMatch = ($searchLang) ? "&& (?prop = skos:notation || LANGMATCHES(lang(?match), ?langParam))" : "";
887
888
        return "?s ?prop ?match . FILTER ($filtercond $labelcondMatch)";
889
    }
890
891
892
    /**
893
     * Inner query for concepts using a search term.
894
     * @param string $term search term
895
     * @param string $lang language code of the returned labels
896
     * @param string $searchLang language code used for matching labels (null means any language)
897
     * @param string[] $props properties to target e.g. array('skos:prefLabel','skos:altLabel')
898
     * @param boolean $unique restrict results to unique concepts (default: false)
899
     * @return string sparql query
900
     */
901
    protected function generateConceptSearchQueryInner($term, $lang, $searchLang, $props, $unique, $filterGraph)
902
    {
903
        $valuesProp = $this->formatValues('?prop', $props);
904
        $textcond = $this->generateConceptSearchQueryCondition($term, $searchLang);
905
906
        $rawterm = str_replace(array('\\', '*', '"'), array('\\\\', '', '\"'), $term);
907
        // graph clause, if necessary
908
        $graphClause = $filterGraph != '' ? 'GRAPH ?graph' : '';
909
910
        // extra conditions for label language, if specified
911
        $labelcondLabel = ($lang) ? "LANGMATCHES(lang(?label), '$lang')" : "lang(?match) = '' || LANGMATCHES(lang(?label), lang(?match))";
912
        // if search language and UI/display language differ, must also consider case where there is no prefLabel in
913
        // the display language; in that case, should use the label with the same language as the matched label
914
        $labelcondFallback = ($searchLang != $lang) ?
915
          "OPTIONAL { # in case previous OPTIONAL block gives no labels\n" .
916
          "?s skos:prefLabel ?label . FILTER (LANGMATCHES(LANG(?label), LANG(?match))) }" : "";
917
918
        //  Including the labels if there is no query term given.
919
        if ($rawterm === '') {
920
          $labelClause = "?s skos:prefLabel ?label .";
921
          $labelClause = ($lang) ? $labelClause . " FILTER (LANGMATCHES(LANG(?label), '$lang'))" : $labelClause . "";
922
          return $labelClause . " BIND(?label AS ?match)";
923
        }
924
925
        /*
926
         * This query does some tricks to obtain a list of unique concepts.
927
         * From each match generated by the text index, a string such as
928
         * "1en@example" is generated, where the first character is a number
929
         * encoding the property and priority, then comes the language tag and
930
         * finally the original literal after an @ sign. Of these, the MIN
931
         * function is used to pick the best match for each concept. Finally,
932
         * the structure is unpacked to get back the original string. Phew!
933
         */
934
        $hitvar = $unique ? '(MIN(?matchstr) AS ?hit)' : '(?matchstr AS ?hit)';
935
        $hitgroup = $unique ? 'GROUP BY ?s ?label ?notation' : '';
936
937
        $langClause = $this->generateLangClause($searchLang);
938
939
        $query = <<<EOQ
940
   SELECT DISTINCT ?s ?label ?notation $hitvar
941
   WHERE {
942
    $graphClause {
943
     { 
944
     $valuesProp
945
     VALUES (?prop ?pri ?langParam) { (skos:prefLabel 1 $langClause) (skos:altLabel 3 $langClause) (skos:notation 5 '') (skos:hiddenLabel 7 $langClause)}
946
     $textcond
947
     ?s ?prop ?match }
948
     OPTIONAL {
949
      ?s skos:prefLabel ?label .
950
      FILTER ($labelcondLabel)
951
     } $labelcondFallback
952
     BIND(IF(langMatches(LANG(?match),'$lang'), ?pri, ?pri+1) AS ?npri)
953
     BIND(CONCAT(STR(?npri), LANG(?match), '@', STR(?match)) AS ?matchstr)
954
     OPTIONAL { ?s skos:notation ?notation }
955
    }
956
    $filterGraph
957
   }
958
   $hitgroup
959
EOQ;
960
961
        return $query;
962
    }
963
    /**
964
    *  This function can be overwritten in other SPARQL dialects for the possibility of handling the different language clauses
965
     * @param string $lang
966
     * @return string formatted language clause
967
     */
968
    protected function generateLangClause($lang) {
969
        return "'$lang'";
970
    }
971
972
    /**
973
     * Query for concepts using a search term.
974
     * @param array|null $fields extra fields to include in the result (array of strings). (default: null = none)
975
     * @param boolean $unique restrict results to unique concepts (default: false)
976
     * @param boolean $showDeprecated whether to include deprecated concepts in search results (default: false)
977
     * @param ConceptSearchParameters $params
978
     * @return string sparql query
979
     */
980
    protected function generateConceptSearchQuery($fields, $unique, $params, $showDeprecated = false) {
981
        $vocabs = $params->getVocabs();
982
        $gcl = $this->graphClause;
983
        $fcl = empty($vocabs) ? '' : $this->generateFromClause($vocabs);
984
        $formattedtype = $this->formatTypes($params->getTypeLimit());
985
        $formattedfields = $this->formatExtraFields($params->getLang(), $fields);
986
        $extravars = $formattedfields['extravars'];
987
        $extrafields = $formattedfields['extrafields'];
988
        $schemes = $params->getSchemeLimit();
989
990
        // limit the search to only requested concept schemes
991
        $schemecond = '';
992
        if (!empty($schemes)) {
993
            $conditions = array();
994
            foreach($schemes as $scheme) {
995
                $conditions[] = "{?s skos:inScheme <$scheme>}";
996
            }
997
            $schemecond = '{'.implode(" UNION ",$conditions).'}';
998
        }
999
        $filterDeprecated="";
1000
        //show or hide deprecated concepts
1001
        if(!$showDeprecated){
1002
            $filterDeprecated="FILTER NOT EXISTS { ?s owl:deprecated true }";
1003
        }
1004
        // extra conditions for parent and group, if specified
1005
        $parentcond = ($params->getParentLimit()) ? "?s skos:broader+ <" . $params->getParentLimit() . "> ." : "";
1006
        $groupcond = ($params->getGroupLimit()) ? "<" . $params->getGroupLimit() . "> skos:member ?s ." : "";
1007
        $pgcond = $parentcond . $groupcond;
1008
1009
        $orderextra = $this->isDefaultEndpoint() ? $this->graph : '';
1010
1011
        # make VALUES clauses
1012
        $props = array('skos:prefLabel', 'skos:altLabel');
1013
1014
        //add notation into searchable data for the vocabularies which have been configured for it
1015
        if ($vocabs) {
1016
            $searchByNotation = false;
1017
            foreach ($vocabs as $vocab) {
1018
                if ($vocab->getConfig()->searchByNotation()) {
1019
                    $searchByNotation = true;
1020
                }
1021
            }
1022
            if ($searchByNotation) {
1023
                $props[] = 'skos:notation';
1024
            }
1025
        }
1026
1027
        if ($params->getHidden()) {
1028
            $props[] = 'skos:hiddenLabel';
1029
        }
1030
        $filterGraph = empty($vocabs) ? $this->formatFilterGraph($vocabs) : '';
1031
1032
        // remove futile asterisks from the search term
1033
        $term = $params->getSearchTerm();
1034
        while (strpos($term, '**') !== false) {
1035
            $term = str_replace('**', '*', $term);
1036
        }
1037
1038
        $labelpriority = <<<EOQ
1039
  FILTER(BOUND(?s))
1040
  BIND(STR(SUBSTR(?hit,1,1)) AS ?pri)
1041
  BIND(IF((SUBSTR(STRBEFORE(?hit, '@'),1) != ?pri), STRLANG(STRAFTER(?hit, '@'), SUBSTR(STRBEFORE(?hit, '@'),2)), STRAFTER(?hit, '@')) AS ?match)
1042
  BIND(IF((?pri = "1" || ?pri = "2") && ?match != ?label, ?match, ?unbound) as ?plabel)
1043
  BIND(IF((?pri = "3" || ?pri = "4"), ?match, ?unbound) as ?alabel)
1044
  BIND(IF((?pri = "7" || ?pri = "8"), ?match, ?unbound) as ?hlabel)
1045
EOQ;
1046
        $innerquery = $this->generateConceptSearchQueryInner($params->getSearchTerm(), $params->getLang(), $params->getSearchLang(), $props, $unique, $filterGraph);
1047
        if ($params->getSearchTerm() === '*' || $params->getSearchTerm() === '') {
1048
          $labelpriority = '';
1049
        }
1050
        $query = <<<EOQ
1051
SELECT DISTINCT ?s ?label ?plabel ?alabel ?hlabel ?graph ?notation (GROUP_CONCAT(DISTINCT STR(?type);separator=' ') as ?types) $extravars 
1052
$fcl
1053
WHERE {
1054
 $gcl {
1055
  {
1056
  $innerquery
1057
  }
1058
  $labelpriority
1059
  $formattedtype
1060
  { $pgcond 
1061
   ?s a ?type .
1062
   $extrafields $schemecond
1063
  }
1064
  $filterDeprecated
1065
 }
1066
 $filterGraph
1067
}
1068
GROUP BY ?s ?match ?label ?plabel ?alabel ?hlabel ?notation ?graph
1069
ORDER BY LCASE(STR(?match)) LANG(?match) $orderextra
1070
EOQ;
1071
        return $query;
1072
    }
1073
1074
    /**
1075
     * Transform a single concept search query results into the skosmos desired return format.
1076
     * @param $row SPARQL query result row
1077
     * @param array $vocabs array of Vocabulary objects to search; empty for global search
1078
     * @return array query result object
1079
     */
1080
    private function transformConceptSearchResult($row, $vocabs, $fields)
1081
    {
1082
        $hit = array();
1083
        $hit['uri'] = $row->s->getUri();
1084
1085
        if (isset($row->graph)) {
1086
            $hit['graph'] = $row->graph->getUri();
1087
        }
1088
1089
        foreach (explode(" ", $row->types->getValue()) as $typeuri) {
1090
            $hit['type'][] = $this->shortenUri($typeuri);
1091
        }
1092
1093
        if(!empty($fields)) {
1094
            foreach ($fields as $prop) {
1095
                $propname = $prop . 's';
1096
                if (isset($row->$propname)) {
1097
                    foreach (explode("\n", $row->$propname->getValue()) as $line) {
1098
                        $rdata = str_getcsv($line, ',', '"', '"');
1099
                        $propvals = array();
1100
                        if ($rdata[0] != '') {
1101
                            $propvals['uri'] = $rdata[0];
1102
                        }
1103
                        if ($rdata[1] != '') {
1104
                            $propvals['prefLabel'] = $rdata[1];
1105
                        }
1106
                        if ($rdata[2] != '') {
1107
                            $propvals = $rdata[2];
1108
                        }
1109
1110
                        $hit['skos:' . $prop][] = $propvals;
1111
                    }
1112
                }
1113
            }
1114
        }
1115
1116
1117
        if (isset($row->preflabels)) {
1118
            foreach (explode("\n", $row->preflabels->getValue()) as $line) {
1119
                $pref = str_getcsv($line, ',', '"', '"');
1120
                $hit['prefLabels'][$pref[1]] = $pref[0];
1121
            }
1122
        }
1123
1124
        foreach ($vocabs as $vocab) { // looping the vocabulary objects and asking these for a localname for the concept.
1125
            $localname = $vocab->getLocalName($hit['uri']);
1126
            if ($localname !== $hit['uri']) { // only passing the result forward if the uri didn't boomerang right back.
1127
                $hit['localname'] = $localname;
1128
                break; // stopping the search when we find one that returns something valid.
1129
            }
1130
        }
1131
1132
        if (isset($row->label)) {
1133
            $hit['prefLabel'] = $row->label->getValue();
1134
        }
1135
1136
        if (isset($row->label)) {
1137
            $hit['lang'] = $row->label->getLang();
1138
        }
1139
1140
        if (isset($row->notation)) {
1141
            $hit['notation'] = $row->notation->getValue();
1142
        }
1143
1144
        if (isset($row->plabel)) {
1145
            $hit['matchedPrefLabel'] = $row->plabel->getValue();
1146
            $hit['lang'] = $row->plabel->getLang();
1147 View Code Duplication
        } elseif (isset($row->alabel)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1148
            $hit['altLabel'] = $row->alabel->getValue();
1149
            $hit['lang'] = $row->alabel->getLang();
1150
        } elseif (isset($row->hlabel)) {
1151
            $hit['hiddenLabel'] = $row->hlabel->getValue();
1152
            $hit['lang'] = $row->hlabel->getLang();
1153
        }
1154
        return $hit;
1155
    }
1156
1157
    /**
1158
     * Transform the concept search query results into the skosmos desired return format.
1159
     * @param EasyRdf\Sparql\Result $results
1160
     * @param array $vocabs array of Vocabulary objects to search; empty for global search
1161
     * @return array query result object
1162
     */
1163
    private function transformConceptSearchResults($results, $vocabs, $fields) {
1164
        $ret = array();
1165
1166
        foreach ($results as $row) {
1167
            if (!isset($row->s)) {
1168
                // don't break if query returns a single dummy result
1169
                continue;
1170
            }
1171
            $ret[] = $this->transformConceptSearchResult($row, $vocabs, $fields);
1172
        }
1173
        return $ret;
1174
    }
1175
1176
    /**
1177
     * Query for concepts using a search term.
1178
     * @param array $vocabs array of Vocabulary objects to search; empty for global search
1179
     * @param array $fields extra fields to include in the result (array of strings). (default: null = none)
1180
     * @param boolean $unique restrict results to unique concepts (default: false)
1181
     * @param boolean $showDeprecated whether to include deprecated concepts in the result (default: false)
1182
     * @param ConceptSearchParameters $params
1183
     * @return array query result object
1184
     */
1185
    public function queryConcepts($vocabs, $fields = null, $unique = false, $params, $showDeprecated = false) {
1186
        $query = $this->generateConceptSearchQuery($fields, $unique, $params,$showDeprecated);
1187
        $results = $this->query($query);
1188
        return $this->transformConceptSearchResults($results, $vocabs, $fields);
0 ignored issues
show
Bug introduced by
It seems like $results defined by $this->query($query) on line 1187 can also be of type object<EasyRdf\Graph> or object<EasyRdf\Http\Response>; however, GenericSparql::transformConceptSearchResults() does only seem to accept object<EasyRdf\Sparql\Result>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1189
    }
1190
1191
    /**
1192
     * Generates sparql query clauses used for creating the alphabetical index.
1193
     * @param string $letter the letter (or special class) to search for
1194
     * @return array of sparql query clause strings
1195
     */
1196
    private function formatFilterConditions($letter, $lang) {
1197
        $useRegex = false;
1198
1199
        if ($letter == '*') {
1200
            $letter = '.*';
1201
            $useRegex = true;
1202
        } elseif ($letter == '0-9') {
1203
            $letter = '[0-9].*';
1204
            $useRegex = true;
1205
        } elseif ($letter == '!*') {
1206
            $letter = '[^\\\\p{L}\\\\p{N}].*';
1207
            $useRegex = true;
1208
        }
1209
1210
        # make text query clause
1211
        $lcletter = mb_strtolower($letter, 'UTF-8'); // convert to lower case, UTF-8 safe
1212
        if ($useRegex) {
1213
            $filtercondLabel = $lang ? "regex(str(?label), '^$letter$', 'i') && langMatches(lang(?label), '$lang')" : "regex(str(?label), '^$letter$', 'i')";
1214
            $filtercondALabel = $lang ? "regex(str(?alabel), '^$letter$', 'i') && langMatches(lang(?alabel), '$lang')" : "regex(str(?alabel), '^$letter$', 'i')";
1215
        } else {
1216
            $filtercondLabel = $lang ? "strstarts(lcase(str(?label)), '$lcletter') && langMatches(lang(?label), '$lang')" : "strstarts(lcase(str(?label)), '$lcletter')";
1217
            $filtercondALabel = $lang ? "strstarts(lcase(str(?alabel)), '$lcletter') && langMatches(lang(?alabel), '$lang')" : "strstarts(lcase(str(?alabel)), '$lcletter')";
1218
        }
1219
        return array('filterpref' => $filtercondLabel, 'filteralt' => $filtercondALabel);
1220
    }
1221
1222
    /**
1223
     * Generates the sparql query used for rendering the alphabetical index.
1224
     * @param string $letter the letter (or special class) to search for
1225
     * @param string $lang language of labels
1226
     * @param integer $limit limits the amount of results
1227
     * @param integer $offset offsets the result set
1228
     * @param array|null $classes
1229
     * @param boolean $showDeprecated whether to include deprecated concepts in the result (default: false)
1230
     * @param \EasyRdf\Resource|null $qualifier alphabetical list qualifier resource or null (default: null)
1231
     * @return string sparql query
1232
     */
1233
    protected function generateAlphabeticalListQuery($letter, $lang, $limit, $offset, $classes, $showDeprecated = false, $qualifier = null) {
1234
        $gcl = $this->graphClause;
1235
        $classes = ($classes) ? $classes : array('http://www.w3.org/2004/02/skos/core#Concept');
1236
        $values = $this->formatValues('?type', $classes, 'uri');
1237
        $limitandoffset = $this->formatLimitAndOffset($limit, $offset);
1238
        $conditions = $this->formatFilterConditions($letter, $lang);
1239
        $filtercondLabel = $conditions['filterpref'];
1240
        $filtercondALabel = $conditions['filteralt'];
1241
        $qualifierClause = $qualifier ? "OPTIONAL { ?s <" . $qualifier->getURI() . "> ?qualifier }" : "";
1242
        $filterDeprecated="";
1243
        if(!$showDeprecated){
1244
            $filterDeprecated="FILTER NOT EXISTS { ?s owl:deprecated true }";
1245
        }
1246
        $query = <<<EOQ
1247
SELECT DISTINCT ?s ?label ?alabel ?qualifier
1248
WHERE {
1249
  $gcl {
1250
    {
1251
      ?s skos:prefLabel ?label .
1252
      FILTER (
1253
        $filtercondLabel
1254
      )
1255
    }
1256
    UNION
1257
    {
1258
      {
1259
        ?s skos:altLabel ?alabel .
1260
        FILTER (
1261
          $filtercondALabel
1262
        )
1263
      }
1264
      {
1265
        ?s skos:prefLabel ?label .
1266
        FILTER (langMatches(lang(?label), '$lang'))
1267
      }
1268
    }
1269
    ?s a ?type .
1270
    $qualifierClause
1271
    $filterDeprecated
1272
    $values
1273
  }
1274
}
1275
ORDER BY LCASE(STR(COALESCE(?alabel, ?label))) STR(?s) LCASE(STR(?qualifier)) $limitandoffset
1276
EOQ;
1277
        return $query;
1278
    }
1279
1280
    /**
1281
     * Transforms the alphabetical list query results into an array format.
1282
     * @param EasyRdf\Sparql\Result $results
1283
     * @return array
1284
     */
1285
    private function transformAlphabeticalListResults($results) {
1286
        $ret = array();
1287
1288
        foreach ($results as $row) {
1289
            if (!isset($row->s)) {
1290
                continue;
1291
            }
1292
            // don't break if query returns a single dummy result
1293
1294
            $hit = array();
1295
            $hit['uri'] = $row->s->getUri();
1296
1297
            $hit['localname'] = $row->s->localName();
1298
1299
            $hit['prefLabel'] = $row->label->getValue();
1300
            $hit['lang'] = $row->label->getLang();
1301
1302 View Code Duplication
            if (isset($row->alabel)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1303
                $hit['altLabel'] = $row->alabel->getValue();
1304
                $hit['lang'] = $row->alabel->getLang();
1305
            }
1306
1307
            if (isset($row->qualifier)) {
1308
                if ($row->qualifier instanceof EasyRdf\Literal) {
1309
                    $hit['qualifier'] = $row->qualifier->getValue();
1310
                }
1311
                else {
1312
                    $hit['qualifier'] = $row->qualifier->localName();
1313
                }
1314
            }
1315
1316
            $ret[] = $hit;
1317
        }
1318
1319
        return $ret;
1320
    }
1321
1322
    /**
1323
     * Query for concepts with a term starting with the given letter. Also special classes '0-9' (digits),
1324
     * '*!' (special characters) and '*' (everything) are accepted.
1325
     * @param string $letter the letter (or special class) to search for
1326
     * @param string $lang language of labels
1327
     * @param integer $limit limits the amount of results
1328
     * @param integer $offset offsets the result set
1329
     * @param array $classes
1330
     * @param boolean $showDeprecated whether to include deprecated concepts in the result (default: false)
1331
     * @param \EasyRdf\Resource|null $qualifier alphabetical list qualifier resource or null (default: null)
1332
     */
1333
    public function queryConceptsAlphabetical($letter, $lang, $limit = null, $offset = null, $classes = null, $showDeprecated = false, $qualifier = null) {
1334
        if ($letter === '') {
1335
            return array(); // special case: no letter given, return empty list
1336
        }
1337
        $query = $this->generateAlphabeticalListQuery($letter, $lang, $limit, $offset, $classes, $showDeprecated, $qualifier);
1338
        $results = $this->query($query);
1339
        return $this->transformAlphabeticalListResults($results);
0 ignored issues
show
Bug introduced by
It seems like $results defined by $this->query($query) on line 1338 can also be of type object<EasyRdf\Graph> or object<EasyRdf\Http\Response>; however, GenericSparql::transformAlphabeticalListResults() does only seem to accept object<EasyRdf\Sparql\Result>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1340
    }
1341
1342
    /**
1343
     * Creates the query used for finding out which letters should be displayed in the alphabetical index.
1344
     * Note that we force the datatype of the result variable otherwise Virtuoso does not properly interpret the DISTINCT and we have duplicated results
1345
     * @param string $lang language
1346
     * @return string sparql query
1347
     */
1348
    private function generateFirstCharactersQuery($lang, $classes) {
1349
        $gcl = $this->graphClause;
1350
        $classes = (isset($classes) && sizeof($classes) > 0) ? $classes : array('http://www.w3.org/2004/02/skos/core#Concept');
1351
        $values = $this->formatValues('?type', $classes, 'uri');
1352
        $query = <<<EOQ
1353
SELECT DISTINCT (ucase(str(substr(?label, 1, 1))) as ?l) WHERE {
1354
  $gcl {
1355
    ?c skos:prefLabel ?label .
1356
    ?c a ?type
1357
    FILTER(langMatches(lang(?label), '$lang'))
1358
    $values
1359
  }
1360
}
1361
EOQ;
1362
        return $query;
1363
    }
1364
1365
    /**
1366
     * Transforms the first characters query results into an array format.
1367
     * @param EasyRdf\Sparql\Result $result
1368
     * @return array
1369
     */
1370
    private function transformFirstCharactersResults($result) {
1371
        $ret = array();
1372
        foreach ($result as $row) {
1373
            $ret[] = $row->l->getValue();
1374
        }
1375
        return $ret;
1376
    }
1377
1378
    /**
1379
     * Query for the first characters (letter or otherwise) of the labels in the particular language.
1380
     * @param string $lang language
1381
     * @return array array of characters
1382
     */
1383
    public function queryFirstCharacters($lang, $classes = null) {
1384
        $query = $this->generateFirstCharactersQuery($lang, $classes);
1385
        $result = $this->query($query);
1386
        return $this->transformFirstCharactersResults($result);
0 ignored issues
show
Bug introduced by
It seems like $result defined by $this->query($query) on line 1385 can also be of type object<EasyRdf\Graph> or object<EasyRdf\Http\Response>; however, GenericSparql::transformFirstCharactersResults() does only seem to accept object<EasyRdf\Sparql\Result>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1387
    }
1388
1389
    /**
1390
     * @param string $uri
1391
     * @param string $lang
1392
     * @return string sparql query string
1393
     */
1394
    private function generateLabelQuery($uri, $lang) {
1395
        $fcl = $this->generateFromClause();
1396
        $labelcondLabel = ($lang) ? "FILTER( langMatches(lang(?label), '$lang') )" : "";
1397
        $query = <<<EOQ
1398
SELECT ?label $fcl
1399
WHERE {
1400
  <$uri> a ?type .
1401
  OPTIONAL {
1402
    <$uri> skos:prefLabel ?label .
1403
    $labelcondLabel
1404
  }
1405
  OPTIONAL {
1406
    <$uri> rdfs:label ?label .
1407
    $labelcondLabel
1408
  }
1409
  OPTIONAL {
1410
    <$uri> dc:title ?label .
1411
    $labelcondLabel
1412
  }
1413
  OPTIONAL {
1414
    <$uri> dc11:title ?label .
1415
    $labelcondLabel
1416
  }
1417
}
1418
EOQ;
1419
        return $query;
1420
    }
1421
1422
1423
    /**
1424
     * @param string $uri
1425
     * @param string $lang
1426
     * @return string sparql query string
1427
     */
1428
    private function generateAllLabelsQuery($uri, $lang) {
1429
        $fcl = $this->generateFromClause();
1430
        $labelcondLabel = ($lang) ? "FILTER( langMatches(lang(?val), '$lang') )" : "";
1431
        $query = <<<EOQ
1432
SELECT DISTINCT ?prop ?val $fcl
1433
WHERE {
1434
  <$uri> a ?type .
1435
  OPTIONAL {
1436
      <$uri> ?prop ?val .
1437
      $labelcondLabel
1438
  }
1439
  VALUES ?prop { skos:prefLabel skos:altLabel skos:hiddenLabel }
1440
}
1441
EOQ;
1442
        return $query;
1443
    }
1444
1445
    /**
1446
     * Query for a label (skos:prefLabel, rdfs:label, dc:title, dc11:title) of a resource.
1447
     * @param string $uri
1448
     * @param string $lang
1449
     * @return array array of labels (key: lang, val: label), or null if resource doesn't exist
1450
     */
1451 View Code Duplication
    public function queryLabel($uri, $lang) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1452
        $query = $this->generateLabelQuery($uri, $lang);
1453
        $result = $this->query($query);
1454
        $ret = array();
1455
        foreach ($result as $row) {
0 ignored issues
show
Bug introduced by
The expression $result of type object<EasyRdf\Http\Resp...>|object<EasyRdf\Graph> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
1456
            if (!isset($row->label)) {
1457
                // existing concept but no labels
1458
                return array();
1459
            }
1460
            $ret[$row->label->getLang()] = $row->label;
1461
        }
1462
1463
        if (sizeof($ret) > 0) {
1464
            // existing concept, with label(s)
1465
            return $ret;
1466
        } else {
1467
            // nonexistent concept
1468
            return null;
1469
        }
1470
    }
1471
1472
    /**
1473
     * Query for skos:prefLabels, skos:altLabels and skos:hiddenLabels of a resource.
1474
     * @param string $uri
1475
     * @param string $lang
1476
     * @return array array of prefLabels, altLabels and hiddenLabels - or null if resource doesn't exist
1477
     */
1478
    public function queryAllConceptLabels($uri, $lang) {
1479
        $query = $this->generateAllLabelsQuery($uri, $lang);
1480
        $result = $this->query($query);
1481
1482
        if ($result->numRows() == 0) {
0 ignored issues
show
Bug introduced by
The method numRows does only exist in EasyRdf\Sparql\Result, but not in EasyRdf\Graph and EasyRdf\Http\Response.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
1483
            // nonexistent concept
1484
            return null;
1485
        }
1486
1487
        $ret = array();
1488
        foreach ($result as $row) {
0 ignored issues
show
Bug introduced by
The expression $result of type object<EasyRdf\Http\Resp...>|object<EasyRdf\Graph> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
1489
            $labelName = $row->prop->localName();
1490
            if (isset($row->val)) {
1491
                $ret[$labelName][] = $row->val->getValue();
1492
            }
1493
        }
1494
        return $ret;
1495
    }
1496
1497
    /**
1498
     * Generates a SPARQL query to retrieve the super properties of a given property URI.
1499
     * Note this must be executed in the graph where this information is available.
1500
     * @param string $uri
1501
     * @return string sparql query string
1502
     */
1503
    private function generateSubPropertyOfQuery($uri) {
1504
        $fcl = $this->generateFromClause();
1505
        $query = <<<EOQ
1506
SELECT ?superProperty $fcl
1507
WHERE {
1508
  <$uri> rdfs:subPropertyOf ?superProperty
1509
}
1510
EOQ;
1511
        return $query;
1512
    }
1513
1514
    /**
1515
     * Query the super properties of a provided property URI.
1516
     * @param string $uri URI of a propertyes
1517
     * @return array array super properties, or null if none exist
1518
     */
1519 View Code Duplication
    public function querySuperProperties($uri) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1520
        $query = $this->generateSubPropertyOfQuery($uri);
1521
        $result = $this->query($query);
1522
        $ret = array();
1523
        foreach ($result as $row) {
0 ignored issues
show
Bug introduced by
The expression $result of type object<EasyRdf\Http\Resp...>|object<EasyRdf\Graph> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
1524
            if (isset($row->superProperty)) {
1525
                $ret[] = $row->superProperty->getUri();
1526
            }
1527
1528
        }
1529
1530
        if (sizeof($ret) > 0) {
1531
            // return result
1532
            return $ret;
1533
        } else {
1534
            // no result, return null
1535
            return null;
1536
        }
1537
    }
1538
1539
1540
    /**
1541
     * Generates a sparql query for queryNotation.
1542
     * @param string $uri
1543
     * @return string sparql query
1544
     */
1545
    private function generateNotationQuery($uri) {
1546
        $fcl = $this->generateFromClause();
1547
1548
        $query = <<<EOQ
1549
SELECT * $fcl
1550
WHERE {
1551
  <$uri> skos:notation ?notation .
1552
}
1553
EOQ;
1554
        return $query;
1555
    }
1556
1557
    /**
1558
     * Query for the notation of the concept (skos:notation) of a resource.
1559
     * @param string $uri
1560
     * @return string notation or null if it doesn't exist
1561
     */
1562
    public function queryNotation($uri) {
1563
        $query = $this->generateNotationQuery($uri);
1564
        $result = $this->query($query);
1565
        foreach ($result as $row) {
0 ignored issues
show
Bug introduced by
The expression $result of type object<EasyRdf\Http\Resp...>|object<EasyRdf\Graph> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
1566
            if (isset($row->notation)) {
1567
                return $row->notation->getValue();
1568
            }
1569
        }
1570
        return null;
1571
    }
1572
1573
    /**
1574
     * Generates a sparql query for queryProperty.
1575
     * @param string $uri
1576
     * @param string $prop the name of the property eg. 'skos:broader'.
1577
     * @param string $lang
1578
     * @param boolean $anylang if you want a label even when it isn't available in the language you requested.
1579
     * @return string sparql query
1580
     */
1581
    private function generatePropertyQuery($uri, $prop, $lang, $anylang) {
1582
        $fcl = $this->generateFromClause();
1583
        $anylang = $anylang ? "OPTIONAL { ?object skos:prefLabel ?label }" : "";
1584
1585
        $query = <<<EOQ
1586
SELECT * $fcl
1587
WHERE {
1588
  <$uri> a skos:Concept .
1589
  OPTIONAL {
1590
    <$uri> $prop ?object .
1591
    OPTIONAL {
1592
      ?object skos:prefLabel ?label .
1593
      FILTER (langMatches(lang(?label), "$lang"))
1594
    }
1595
    OPTIONAL {
1596
      ?object skos:prefLabel ?label .
1597
      FILTER (lang(?label) = "")
1598
    }
1599
    $anylang
1600
  }
1601
}
1602
EOQ;
1603
        return $query;
1604
    }
1605
1606
    /**
1607
     * Transforms the sparql query result into an array or null if the concept doesn't exist.
1608
     * @param EasyRdf\Sparql\Result $result
1609
     * @param string $lang
1610
     * @return array array of property values (key: URI, val: label), or null if concept doesn't exist
1611
     */
1612
    private function transformPropertyQueryResults($result, $lang) {
1613
        $ret = array();
1614
        foreach ($result as $row) {
1615
            if (!isset($row->object)) {
1616
                return array();
1617
            }
1618
            // existing concept but no properties
1619
            if (isset($row->label)) {
1620
                if ($row->label->getLang() === $lang || array_key_exists($row->object->getUri(), $ret) === false) {
1621
                    $ret[$row->object->getUri()]['label'] = $row->label->getValue();
1622
                }
1623
1624
            } else {
1625
                $ret[$row->object->getUri()]['label'] = null;
1626
            }
1627
        }
1628
        if (sizeof($ret) > 0) {
1629
            return $ret;
1630
        }
1631
        // existing concept, with properties
1632
        else {
1633
            return null;
1634
        }
1635
        // nonexistent concept
1636
    }
1637
1638
    /**
1639
     * Query a single property of a concept.
1640
     * @param string $uri
1641
     * @param string $prop the name of the property eg. 'skos:broader'.
1642
     * @param string $lang
1643
     * @param boolean $anylang if you want a label even when it isn't available in the language you requested.
1644
     * @return array array of property values (key: URI, val: label), or null if concept doesn't exist
1645
     */
1646
    public function queryProperty($uri, $prop, $lang, $anylang = false) {
1647
        $uri = is_array($uri) ? $uri[0] : $uri;
1648
        $query = $this->generatePropertyQuery($uri, $prop, $lang, $anylang);
1649
        $result = $this->query($query);
1650
        return $this->transformPropertyQueryResults($result, $lang);
0 ignored issues
show
Bug introduced by
It seems like $result defined by $this->query($query) on line 1649 can also be of type object<EasyRdf\Graph> or object<EasyRdf\Http\Response>; however, GenericSparql::transformPropertyQueryResults() does only seem to accept object<EasyRdf\Sparql\Result>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1651
    }
1652
1653
    /**
1654
     * Query a single transitive property of a concept.
1655
     * @param string $uri
1656
     * @param array $props the name of the property eg. 'skos:broader'.
1657
     * @param string $lang
1658
     * @param integer $limit
1659
     * @param boolean $anylang if you want a label even when it isn't available in the language you requested.
1660
     * @return string sparql query
1661
     */
1662
    private function generateTransitivePropertyQuery($uri, $props, $lang, $limit, $anylang) {
1663
        $uri = is_array($uri) ? $uri[0] : $uri;
1664
        $fcl = $this->generateFromClause();
1665
        $propertyClause = implode('|', $props);
1666
        $otherlang = $anylang ? "OPTIONAL { ?object skos:prefLabel ?label }" : "";
1667
        // need to do a SPARQL subquery because LIMIT needs to be applied /after/
1668
        // the direct relationships have been collapsed into one string
1669
        $query = <<<EOQ
1670
SELECT * $fcl
1671
WHERE {
1672
  SELECT ?object ?label (GROUP_CONCAT(STR(?dir);separator=' ') as ?direct)
1673
  WHERE {
1674
    <$uri> a skos:Concept .
1675
    OPTIONAL {
1676
      <$uri> $propertyClause* ?object .
1677
      OPTIONAL {
1678
        ?object $propertyClause ?dir .
1679
      }
1680
    }
1681
    OPTIONAL {
1682
      ?object skos:prefLabel ?label .
1683
      FILTER (langMatches(lang(?label), "$lang"))
1684
    }
1685
    $otherlang
1686
  }
1687
  GROUP BY ?object ?label
1688
}
1689
LIMIT $limit
1690
EOQ;
1691
        return $query;
1692
    }
1693
1694
    /**
1695
     * Transforms the sparql query result object into an array.
1696
     * @param EasyRdf\Sparql\Result $result
1697
     * @param string $lang
1698
     * @param string $fallbacklang language to use if label is not available in the preferred language
1699
     * @return array of property values (key: URI, val: label), or null if concept doesn't exist
1700
     */
1701
    private function transformTransitivePropertyResults($result, $lang, $fallbacklang) {
1702
        $ret = array();
1703
        foreach ($result as $row) {
1704
            if (!isset($row->object)) {
1705
                return array();
1706
            }
1707
            // existing concept but no properties
1708
            if (isset($row->label)) {
1709
                $val = array('label' => $row->label->getValue());
1710
            } else {
1711
                $val = array('label' => null);
1712
            }
1713 View Code Duplication
            if (isset($row->direct) && $row->direct->getValue() != '') {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1714
                $val['direct'] = explode(' ', $row->direct->getValue());
1715
            }
1716
            // Preventing labels in a non preferred language overriding the preferred language.
1717
            if (isset($row->label) && $row->label->getLang() === $lang || array_key_exists($row->object->getUri(), $ret) === false) {
1718
                if (!isset($row->label) || $row->label->getLang() === $lang) {
1719
                    $ret[$row->object->getUri()] = $val;
1720
                } elseif ($row->label->getLang() === $fallbacklang) {
1721
                    $val['label'] .= ' (' . $row->label->getLang() . ')';
1722
                    $ret[$row->object->getUri()] = $val;
1723
                }
1724
            }
1725
        }
1726
1727
        // second iteration of results to find labels for the ones that didn't have one in the preferred languages
1728
        foreach ($result as $row) {
1729
            if (isset($row->object) && array_key_exists($row->object->getUri(), $ret) === false) {
1730
                $val = array('label' => $row->label->getValue());
1731 View Code Duplication
                if (isset($row->direct) && $row->direct->getValue() != '') {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1732
                    $val['direct'] = explode(' ', $row->direct->getValue());
1733
                }
1734
                $ret[$row->object->getUri()] = $val;
1735
            }
1736
        }
1737
1738
        if (sizeof($ret) > 0) {
1739
            return $ret;
1740
        }
1741
        // existing concept, with properties
1742
        else {
1743
            return null;
1744
        }
1745
        // nonexistent concept
1746
    }
1747
1748
    /**
1749
     * Query a single transitive property of a concept.
1750
     * @param string $uri
1751
     * @param array $props the property/properties.
1752
     * @param string $lang
1753
     * @param string $fallbacklang language to use if label is not available in the preferred language
1754
     * @param integer $limit
1755
     * @param boolean $anylang if you want a label even when it isn't available in the language you requested.
1756
     * @return array array of property values (key: URI, val: label), or null if concept doesn't exist
1757
     */
1758
    public function queryTransitiveProperty($uri, $props, $lang, $limit, $anylang = false, $fallbacklang = '') {
1759
        $query = $this->generateTransitivePropertyQuery($uri, $props, $lang, $limit, $anylang);
1760
        $result = $this->query($query);
1761
        return $this->transformTransitivePropertyResults($result, $lang, $fallbacklang);
0 ignored issues
show
Bug introduced by
It seems like $result defined by $this->query($query) on line 1760 can also be of type object<EasyRdf\Graph> or object<EasyRdf\Http\Response>; however, GenericSparql::transform...sitivePropertyResults() does only seem to accept object<EasyRdf\Sparql\Result>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1762
    }
1763
1764
    /**
1765
     * Generates the query for a concepts skos:narrowers.
1766
     * @param string $uri
1767
     * @param string $lang
1768
     * @param string $fallback
1769
     * @return string sparql query
1770
     */
1771
    private function generateChildQuery($uri, $lang, $fallback, $props) {
1772
        $uri = is_array($uri) ? $uri[0] : $uri;
1773
        $fcl = $this->generateFromClause();
1774
        $propertyClause = implode('|', $props);
1775
        $query = <<<EOQ
1776
SELECT ?child ?label ?child ?grandchildren ?notation $fcl WHERE {
1777
  <$uri> a skos:Concept .
1778
  OPTIONAL {
1779
    ?child $propertyClause <$uri> .
1780
    OPTIONAL {
1781
      ?child skos:prefLabel ?label .
1782
      FILTER (langMatches(lang(?label), "$lang"))
1783
    }
1784
    OPTIONAL {
1785
      ?child skos:prefLabel ?label .
1786
      FILTER (langMatches(lang(?label), "$fallback"))
1787
    }
1788
    OPTIONAL { # other language case
1789
      ?child skos:prefLabel ?label .
1790
    }
1791
    OPTIONAL {
1792
      ?child skos:notation ?notation .
1793
    }
1794
    BIND ( EXISTS { ?a $propertyClause ?child . } AS ?grandchildren )
1795
  }
1796
}
1797
EOQ;
1798
        return $query;
1799
    }
1800
1801
    /**
1802
     * Transforms the sparql result object into an array.
1803
     * @param EasyRdf\Sparql\Result $result
1804
     * @param string $lang
1805
     * @return array array of arrays describing each child concept, or null if concept doesn't exist
1806
     */
1807
    private function transformNarrowerResults($result, $lang) {
1808
        $ret = array();
1809
        foreach ($result as $row) {
1810
            if (!isset($row->child)) {
1811
                return array();
1812
            }
1813
            // existing concept but no children
1814
1815
            $label = null;
1816
            if (isset($row->label)) {
1817
                if ($row->label->getLang() == $lang || strpos($row->label->getLang(), $lang . "-") == 0) {
1818
                    $label = $row->label->getValue();
1819
                } else {
1820
                    $label = $row->label->getValue() . " (" . $row->label->getLang() . ")";
1821
                }
1822
1823
            }
1824
            $childArray = array(
1825
                'uri' => $row->child->getUri(),
1826
                'prefLabel' => $label,
1827
                'hasChildren' => filter_var($row->grandchildren->getValue(), FILTER_VALIDATE_BOOLEAN),
1828
            );
1829
            if (isset($row->notation)) {
1830
                $childArray['notation'] = $row->notation->getValue();
1831
            }
1832
1833
            $ret[] = $childArray;
1834
        }
1835
        if (sizeof($ret) > 0) {
1836
            return $ret;
1837
        }
1838
        // existing concept, with children
1839
        else {
1840
            return null;
1841
        }
1842
        // nonexistent concept
1843
    }
1844
1845
    /**
1846
     * Query the narrower concepts of a concept.
1847
     * @param string $uri
1848
     * @param string $lang
1849
     * @param string $fallback
1850
     * @return array array of arrays describing each child concept, or null if concept doesn't exist
1851
     */
1852
    public function queryChildren($uri, $lang, $fallback, $props) {
1853
        $query = $this->generateChildQuery($uri, $lang, $fallback, $props);
1854
        $result = $this->query($query);
1855
        return $this->transformNarrowerResults($result, $lang);
0 ignored issues
show
Bug introduced by
It seems like $result defined by $this->query($query) on line 1854 can also be of type object<EasyRdf\Graph> or object<EasyRdf\Http\Response>; however, GenericSparql::transformNarrowerResults() does only seem to accept object<EasyRdf\Sparql\Result>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1856
    }
1857
1858
    /**
1859
     * Query the top concepts of a vocabulary.
1860
     * @param string $conceptSchemes concept schemes whose top concepts to query for
1861
     * @param string $lang language of labels
1862
     * @param string $fallback language to use if label is not available in the preferred language
1863
     */
1864
    public function queryTopConcepts($conceptSchemes, $lang, $fallback) {
1865
        if (!is_array($conceptSchemes)) {
1866
            $conceptSchemes = array($conceptSchemes);
1867
        }
1868
1869
        $values = $this->formatValues('?topuri', $conceptSchemes, 'uri');
1870
1871
        $fcl = $this->generateFromClause();
1872
        $query = <<<EOQ
1873
SELECT DISTINCT ?top ?topuri ?label ?notation ?children $fcl WHERE {
1874
  ?top skos:topConceptOf ?topuri .
1875
  OPTIONAL {
1876
    ?top skos:prefLabel ?label .
1877
    FILTER (langMatches(lang(?label), "$lang"))
1878
  }
1879
  OPTIONAL {
1880
    ?top skos:prefLabel ?label .
1881
    FILTER (langMatches(lang(?label), "$fallback"))
1882
  }
1883
  OPTIONAL { # fallback - other language case
1884
    ?top skos:prefLabel ?label .
1885
  }
1886
  OPTIONAL { ?top skos:notation ?notation . }
1887
  BIND ( EXISTS { ?top skos:narrower ?a . } AS ?children )
1888
  $values
1889
}
1890
EOQ;
1891
        $result = $this->query($query);
1892
        $ret = array();
1893
        foreach ($result as $row) {
0 ignored issues
show
Bug introduced by
The expression $result of type object<EasyRdf\Http\Resp...>|object<EasyRdf\Graph> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
1894
            if (isset($row->top) && isset($row->label)) {
1895
                $label = $row->label->getValue();
1896 View Code Duplication
                if ($row->label->getLang() && $row->label->getLang() !== $lang && strpos($row->label->getLang(), $lang . "-") !== 0) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1897
                    $label .= ' (' . $row->label->getLang() . ')';
1898
                }
1899
                $top = array('uri' => $row->top->getUri(), 'topConceptOf' => $row->topuri->getUri(), 'label' => $label, 'hasChildren' => filter_var($row->children->getValue(), FILTER_VALIDATE_BOOLEAN));
1900
                if (isset($row->notation)) {
1901
                    $top['notation'] = $row->notation->getValue();
1902
                }
1903
1904
                $ret[] = $top;
1905
            }
1906
        }
1907
1908
        return $ret;
1909
    }
1910
1911
    /**
1912
     * Generates a sparql query for finding the hierarchy for a concept.
1913
	 * A concept may be a top concept in multiple schemes, returned as a single whitespace-separated literal.
1914
     * @param string $uri concept uri.
1915
     * @param string $lang
1916
     * @param string $fallback language to use if label is not available in the preferred language
1917
     * @return string sparql query
1918
     */
1919
    private function generateParentListQuery($uri, $lang, $fallback, $props) {
1920
        $fcl = $this->generateFromClause();
1921
        $propertyClause = implode('|', $props);
1922
        $query = <<<EOQ
1923
SELECT ?broad ?parent ?children ?grandchildren
1924
(SAMPLE(?lab) as ?label) (SAMPLE(?childlab) as ?childlabel) (GROUP_CONCAT(?topcs; separator=" ") as ?tops) 
1925
(SAMPLE(?nota) as ?notation) (SAMPLE(?childnota) as ?childnotation) $fcl
1926
WHERE {
1927
  <$uri> a skos:Concept .
1928
  OPTIONAL {
1929
    <$uri> $propertyClause* ?broad .
1930
    OPTIONAL {
1931
      ?broad skos:prefLabel ?lab .
1932
      FILTER (langMatches(lang(?lab), "$lang"))
1933
    }
1934
    OPTIONAL {
1935
      ?broad skos:prefLabel ?lab .
1936
      FILTER (langMatches(lang(?lab), "$fallback"))
1937
    }
1938
    OPTIONAL { # fallback - other language case
1939
      ?broad skos:prefLabel ?lab .
1940
    }
1941
    OPTIONAL { ?broad skos:notation ?nota . }
1942
    OPTIONAL { ?broad $propertyClause ?parent . }
1943
    OPTIONAL { ?broad skos:narrower ?children .
1944
      OPTIONAL {
1945
        ?children skos:prefLabel ?childlab .
1946
        FILTER (langMatches(lang(?childlab), "$lang"))
1947
      }
1948
      OPTIONAL {
1949
        ?children skos:prefLabel ?childlab .
1950
        FILTER (langMatches(lang(?childlab), "$fallback"))
1951
      }
1952
      OPTIONAL { # fallback - other language case
1953
        ?children skos:prefLabel ?childlab .
1954
      }
1955
      OPTIONAL {
1956
        ?children skos:notation ?childnota .
1957
      }
1958
    }
1959
    BIND ( EXISTS { ?children skos:narrower ?a . } AS ?grandchildren )
1960
    OPTIONAL { ?broad skos:topConceptOf ?topcs . }
1961
  }
1962
}
1963
GROUP BY ?broad ?parent ?member ?children ?grandchildren
1964
EOQ;
1965
        return $query;
1966
    }
1967
1968
    /**
1969
     * Transforms the result into an array.
1970
     * @param EasyRdf\Sparql\Result
1971
     * @param string $lang
1972
     * @return array|null an array for the REST controller to encode.
1973
     */
1974
    private function transformParentListResults($result, $lang)
1975
    {
1976
        $ret = array();
1977
        foreach ($result as $row) {
1978
            if (!isset($row->broad)) {
1979
                // existing concept but no broaders
1980
                return array();
1981
            }
1982
            $uri = $row->broad->getUri();
1983
            if (!isset($ret[$uri])) {
1984
                $ret[$uri] = array('uri' => $uri);
1985
            }
1986
            if (isset($row->exact)) {
1987
                $ret[$uri]['exact'] = $row->exact->getUri();
1988
            }
1989
            if (isset($row->tops)) {
1990
               $topConceptsList=explode(" ", $row->tops->getValue());
1991
               // sort to guarantee an alphabetical ordering of the URI
1992
               sort($topConceptsList);
1993
               $ret[$uri]['tops'] = $topConceptsList;
1994
            }
1995
            if (isset($row->children)) {
1996
                if (!isset($ret[$uri]['narrower'])) {
1997
                    $ret[$uri]['narrower'] = array();
1998
                }
1999
2000
                $label = null;
2001
                if (isset($row->childlabel)) {
2002
                    $label = $row->childlabel->getValue();
2003 View Code Duplication
                    if ($row->childlabel->getLang() !== $lang && strpos($row->childlabel->getLang(), $lang . "-") !== 0) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2004
                        $label .= " (" . $row->childlabel->getLang() . ")";
2005
                    }
2006
2007
                }
2008
2009
                $childArr = array(
2010
                    'uri' => $row->children->getUri(),
2011
                    'label' => $label,
2012
                    'hasChildren' => filter_var($row->grandchildren->getValue(), FILTER_VALIDATE_BOOLEAN),
2013
                );
2014
                if (isset($row->childnotation)) {
2015
                    $childArr['notation'] = $row->childnotation->getValue();
2016
                }
2017
2018
                if (!in_array($childArr, $ret[$uri]['narrower'])) {
2019
                    $ret[$uri]['narrower'][] = $childArr;
2020
                }
2021
2022
            }
2023
            if (isset($row->label)) {
2024
                $preflabel = $row->label->getValue();
2025 View Code Duplication
                if ($row->label->getLang() && $row->label->getLang() !== $lang && strpos($row->label->getLang(), $lang . "-") !== 0) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2026
                    $preflabel .= ' (' . $row->label->getLang() . ')';
2027
                }
2028
2029
                $ret[$uri]['prefLabel'] = $preflabel;
2030
            }
2031
            if (isset($row->notation)) {
2032
                $ret[$uri]['notation'] = $row->notation->getValue();
2033
            }
2034
2035
            if (isset($row->parent) && (isset($ret[$uri]['broader']) && !in_array($row->parent->getUri(), $ret[$uri]['broader']))) {
2036
                $ret[$uri]['broader'][] = $row->parent->getUri();
2037
            } elseif (isset($row->parent) && !isset($ret[$uri]['broader'])) {
2038
                $ret[$uri]['broader'][] = $row->parent->getUri();
2039
            }
2040
        }
2041
        if (sizeof($ret) > 0) {
2042
            // existing concept, with children
2043
            return $ret;
2044
        }
2045
        else {
2046
            // nonexistent concept
2047
            return null;
2048
        }
2049
    }
2050
2051
    /**
2052
     * Query for finding the hierarchy for a concept.
2053
     * @param string $uri concept uri.
2054
     * @param string $lang
2055
     * @param string $fallback language to use if label is not available in the preferred language
2056
     * @param array $props the hierarchy property/properties to use
2057
     * @return an array for the REST controller to encode.
2058
     */
2059
    public function queryParentList($uri, $lang, $fallback, $props) {
2060
        $query = $this->generateParentListQuery($uri, $lang, $fallback, $props);
2061
        $result = $this->query($query);
2062
        return $this->transformParentListResults($result, $lang);
2063
    }
2064
2065
    /**
2066
     * return a list of concept group instances, sorted by label
2067
     * @param string $groupClass URI of concept group class
2068
     * @param string $lang language of labels to return
2069
     * @return string sparql query
2070
     */
2071
    private function generateConceptGroupsQuery($groupClass, $lang) {
2072
        $fcl = $this->generateFromClause();
2073
        $query = <<<EOQ
2074
SELECT ?group (GROUP_CONCAT(DISTINCT STR(?child);separator=' ') as ?children) ?label ?members ?notation $fcl
2075
WHERE {
2076
  ?group a <$groupClass> .
2077
  OPTIONAL { ?group skos:member|isothes:subGroup ?child .
2078
             ?child a <$groupClass> }
2079
  BIND(EXISTS{?group skos:member ?submembers} as ?members)
2080
  OPTIONAL { ?group skos:prefLabel ?label }
2081
  OPTIONAL { ?group rdfs:label ?label }
2082
  FILTER (langMatches(lang(?label), '$lang'))
2083
  OPTIONAL { ?group skos:notation ?notation }
2084
}
2085
GROUP BY ?group ?label ?members ?notation
2086
ORDER BY lcase(?label)
2087
EOQ;
2088
        return $query;
2089
    }
2090
2091
    /**
2092
     * Transforms the sparql query result into an array.
2093
     * @param EasyRdf\Sparql\Result $result
2094
     * @return array
2095
     */
2096
    private function transformConceptGroupsResults($result) {
2097
        $ret = array();
2098
        foreach ($result as $row) {
2099
            if (!isset($row->group)) {
2100
                # no groups found, see issue #357
2101
                continue;
2102
            }
2103
            $group = array('uri' => $row->group->getURI());
2104
            if (isset($row->label)) {
2105
                $group['prefLabel'] = $row->label->getValue();
2106
            }
2107
2108
            if (isset($row->children)) {
2109
                $group['childGroups'] = explode(' ', $row->children->getValue());
2110
            }
2111
2112
            if (isset($row->members)) {
2113
                $group['hasMembers'] = $row->members->getValue();
2114
            }
2115
2116
            if (isset($row->notation)) {
2117
                $group['notation'] = $row->notation->getValue();
2118
            }
2119
2120
            $ret[] = $group;
2121
        }
2122
        return $ret;
2123
    }
2124
2125
    /**
2126
     * return a list of concept group instances, sorted by label
2127
     * @param string $groupClass URI of concept group class
2128
     * @param string $lang language of labels to return
2129
     * @return array Result array with group URI as key and group label as value
2130
     */
2131
    public function listConceptGroups($groupClass, $lang) {
2132
        $query = $this->generateConceptGroupsQuery($groupClass, $lang);
2133
        $result = $this->query($query);
2134
        return $this->transformConceptGroupsResults($result);
0 ignored issues
show
Bug introduced by
It seems like $result defined by $this->query($query) on line 2133 can also be of type object<EasyRdf\Graph> or object<EasyRdf\Http\Response>; however, GenericSparql::transformConceptGroupsResults() does only seem to accept object<EasyRdf\Sparql\Result>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
2135
    }
2136
2137
    /**
2138
     * Generates the sparql query for listConceptGroupContents
2139
     * @param string $groupClass URI of concept group class
2140
     * @param string $group URI of the concept group instance
2141
     * @param string $lang language of labels to return
2142
     * @param boolean $showDeprecated whether to include deprecated in the result
2143
     * @return string sparql query
2144
     */
2145
    private function generateConceptGroupContentsQuery($groupClass, $group, $lang, $showDeprecated = false) {
2146
        $fcl = $this->generateFromClause();
2147
        $filterDeprecated="";
2148
        if(!$showDeprecated){
2149
            $filterDeprecated="  FILTER NOT EXISTS { ?conc owl:deprecated true }";
2150
        }
2151
        $query = <<<EOQ
2152
SELECT ?conc ?super ?label ?members ?type ?notation $fcl
2153
WHERE {
2154
 <$group> a <$groupClass> .
2155
 { <$group> skos:member ?conc . } UNION { ?conc isothes:superGroup <$group> }
2156
$filterDeprecated
2157
 ?conc a ?type .
2158
 OPTIONAL { ?conc skos:prefLabel ?label .
2159
  FILTER (langMatches(lang(?label), '$lang'))
2160
 }
2161
 OPTIONAL { ?conc skos:prefLabel ?label . }
2162
 OPTIONAL { ?conc skos:notation ?notation }
2163
 BIND(EXISTS{?submembers isothes:superGroup ?conc} as ?super)
2164
 BIND(EXISTS{?conc skos:member ?submembers} as ?members)
2165
} ORDER BY lcase(?label)
2166
EOQ;
2167
        return $query;
2168
    }
2169
2170
    /**
2171
     * Transforms the sparql query result into an array.
2172
     * @param EasyRdf\Sparql\Result $result
2173
     * @param string $lang language of labels to return
2174
     * @return array
2175
     */
2176
    private function transformConceptGroupContentsResults($result, $lang) {
2177
        $ret = array();
2178
        $values = array();
2179
        foreach ($result as $row) {
2180
            if (!array_key_exists($row->conc->getURI(), $values)) {
2181
                $values[$row->conc->getURI()] = array(
2182
                    'uri' => $row->conc->getURI(),
2183
                    'isSuper' => $row->super->getValue(),
2184
                    'hasMembers' => $row->members->getValue(),
2185
                    'type' => array($row->type->shorten()),
2186
                );
2187
                if (isset($row->label)) {
2188
                    if ($row->label->getLang() == $lang || strpos($row->label->getLang(), $lang . "-") == 0) {
2189
                        $values[$row->conc->getURI()]['prefLabel'] = $row->label->getValue();
2190
                    } else {
2191
                        $values[$row->conc->getURI()]['prefLabel'] = $row->label->getValue() . " (" . $row->label->getLang() . ")";
2192
                    }
2193
2194
                }
2195
                if (isset($row->notation)) {
2196
                    $values[$row->conc->getURI()]['notation'] = $row->notation->getValue();
2197
                }
2198
2199
            } else {
2200
                $values[$row->conc->getURI()]['type'][] = $row->type->shorten();
2201
            }
2202
        }
2203
2204
        foreach ($values as $val) {
2205
            $ret[] = $val;
2206
        }
2207
2208
        return $ret;
2209
    }
2210
2211
    /**
2212
     * return a list of concepts in a concept group
2213
     * @param string $groupClass URI of concept group class
2214
     * @param string $group URI of the concept group instance
2215
     * @param string $lang language of labels to return
2216
     * @param boolean $showDeprecated whether to include deprecated concepts in search results
2217
     * @return array Result array with concept URI as key and concept label as value
2218
     */
2219
    public function listConceptGroupContents($groupClass, $group, $lang,$showDeprecated = false) {
2220
        $query = $this->generateConceptGroupContentsQuery($groupClass, $group, $lang,$showDeprecated);
2221
        $result = $this->query($query);
2222
        return $this->transformConceptGroupContentsResults($result, $lang);
0 ignored issues
show
Bug introduced by
It seems like $result defined by $this->query($query) on line 2221 can also be of type object<EasyRdf\Graph> or object<EasyRdf\Http\Response>; however, GenericSparql::transform...tGroupContentsResults() does only seem to accept object<EasyRdf\Sparql\Result>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
2223
    }
2224
2225
    /**
2226
     * Generates the sparql query for queryChangeList.
2227
     * @param string $prop the property uri pointing to timestamps, eg. 'dc:modified'
2228
     * @param string $lang language of labels to return
2229
     * @param int $offset offset of results to retrieve; 0 for beginning of list
2230
     * @param int $limit maximum number of results to return
2231
     * @param boolean $showDeprecated whether to include deprecated concepts in the change list
2232
     * @return string sparql query
2233
     */
2234
    private function generateChangeListQuery($prop, $lang, $offset, $limit=200, $showDeprecated) {
2235
        $fcl = $this->generateFromClause();
2236
        $offset = ($offset) ? 'OFFSET ' . $offset : '';
2237
2238
        //Additional clauses when deprecated concepts need to be included in the results
2239
        $optionVar = ($showDeprecated) ? '?replacedBy' : '';
2240
        $optionStart = ($showDeprecated) ? 'OPTIONAL {' : '';
2241
        $optionEnd = ($showDeprecated) ? '}' : '';
2242
        $deprecatedOption = ($showDeprecated) ? 'OPTIONAL { ?concept dc:modified ?date ; owl:deprecated TRUE ; dc:isReplacedBy ?replacedBy }' : '';
2243
2244
        $query = <<<EOQ
2245
SELECT DISTINCT ?concept ?date ?label $optionVar $fcl
2246
WHERE {
2247
  ?concept a skos:Concept .
2248
  ?concept skos:prefLabel ?label .
2249
  FILTER (langMatches(lang(?label), '$lang'))
2250
  $optionStart ?concept $prop ?date .
2251
  MINUS{ ?concept owl:deprecated TRUE } $optionEnd
2252
  $deprecatedOption
2253
}
2254
ORDER BY DESC(YEAR(?date)) DESC(MONTH(?date)) LCASE(?label)
2255
LIMIT $limit $offset
2256
EOQ;
2257
2258
        return $query;
2259
    }
2260
2261
    /**
2262
     * Transforms the sparql query result into an array.
2263
     * @param EasyRdf\Sparql\Result $result
2264
     * @return array
2265
     */
2266
    private function transformChangeListResults($result) {
2267
        $ret = array();
2268
        foreach ($result as $row) {
2269
            $concept = array('uri' => $row->concept->getURI());
2270
            if (isset($row->label)) {
2271
                $concept['prefLabel'] = $row->label->getValue();
2272
            }
2273
2274
            if (isset($row->date)) {
2275
                try {
2276
                    $concept['date'] = $row->date->getValue();
2277
                } catch (Exception $e) {
2278
                    //don't record concepts with malformed dates e.g. 1986-21-00
2279
                    continue;
2280
                }
2281
            }
2282
2283
            if (isset($row->replacedBy)) {
2284
                $concept['replacedBy'] = $row->replacedBy->getURI();
2285
            }
2286
2287
            $ret[] = $concept;
2288
        }
2289
        return $ret;
2290
    }
2291
2292
    /**
2293
     * return a list of recently changed or entirely new concepts
2294
     * @param string $prop the property uri pointing to timestamps, eg. 'dc:modified'
2295
     * @param string $lang language of labels to return
2296
     * @param int $offset offset of results to retrieve; 0 for beginning of list
2297
     * @param int $limit maximum number of results to return
2298
     * @param boolean $showDeprecated whether to include deprecated concepts in the change list
2299
     * @return array Result array
2300
     */
2301
    public function queryChangeList($prop, $lang, $offset, $limit, $showDeprecated=false) {
2302
        $query = $this->generateChangeListQuery($prop, $lang, $offset, $limit, $showDeprecated);
2303
2304
        $result = $this->query($query);
2305
        return $this->transformChangeListResults($result);
0 ignored issues
show
Bug introduced by
It seems like $result defined by $this->query($query) on line 2304 can also be of type object<EasyRdf\Graph> or object<EasyRdf\Http\Response>; however, GenericSparql::transformChangeListResults() does only seem to accept object<EasyRdf\Sparql\Result>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
2306
    }
2307
}
2308