Completed
Push — master ( b81c51...610131 )
by Osma
19s queued 11s
created

transformQueryConceptSchemesResults()   B

Complexity

Conditions 7
Paths 17

Size

Total Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
nc 17
nop 1
dl 0
loc 25
rs 8.5866
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 string $graph
15
     */
16
    protected $graph;
17
    /**
18
     * A SPARQL query graph part template.
19
     * @property string $graph
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 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
123
    protected function initializeHttpClient() {
124
        // configure the HTTP client used by EasyRdf\Sparql\Client
125
        $httpclient = EasyRdf\Http::getDefaultHttpClient();
126
        $httpclient->setConfig(array('timeout' => $this->model->getConfig()->getSparqlTimeout()));
127
128
        // if special cache control (typically no-cache) was requested by the
129
        // client, set the same type of cache control headers also in subsequent
130
        // in the SPARQL requests (this is useful for performance testing)
131
        // @codeCoverageIgnoreStart
132
        $cacheControl = filter_input(INPUT_SERVER, 'HTTP_CACHE_CONTROL', FILTER_SANITIZE_STRING);
133
        $pragma = filter_input(INPUT_SERVER, 'HTTP_PRAGMA', FILTER_SANITIZE_STRING);
134
        if ($cacheControl !== null || $pragma !== null) {
135
            $val = $pragma !== null ? $pragma : $cacheControl;
136
            $httpclient->setHeaders('Cache-Control', $val);
137
        }
138
        // @codeCoverageIgnoreEnd
139
140
        EasyRdf\Http::setDefaultHttpClient($httpclient); // actually redundant..
141
    }
142
143
    /**
144
     * Return true if this is the default SPARQL endpoint, used as the facade to query
145
     * all vocabularies.
146
     */
147
148
    protected function isDefaultEndpoint() {
149
        return !is_null($this->graph) && $this->graph[0] == '?';
150
    }
151
152
    /**
153
     * Returns the graph instance
154
     * @return object EasyRDF graph instance.
155
     */
156
    public function getGraph() {
157
        return $this->graph;
158
    }
159
160
    /**
161
     * Shorten a URI
162
     * @param string $uri URI to shorten
163
     * @return string shortened URI, or original URI if it cannot be shortened
164
     */
165
    private function shortenUri($uri) {
166
        if (!array_key_exists($uri, $this->qnamecache)) {
167
            $res = new EasyRdf\Resource($uri);
168
            $qname = $res->shorten(); // returns null on failure
169
            $this->qnamecache[$uri] = ($qname !== null) ? $qname : $uri;
170
        }
171
        return $this->qnamecache[$uri];
172
    }
173
174
175
    /**
176
     * Generates the sparql query for retrieving concept and collection counts in a vocabulary.
177
     * @return string sparql query
178
     */
179
    private function generateCountConceptsQuery($array, $group) {
180
        $fcl = $this->generateFromClause();
181
        $optional = $array ? "UNION { ?type rdfs:subClassOf* <$array> }" : '';
182
        $optional .= $group ? "UNION { ?type rdfs:subClassOf* <$group> }" : '';
183
        $query = <<<EOQ
184
      SELECT (COUNT(?conc) as ?c) ?type ?typelabel $fcl WHERE {
185
        { ?conc a ?type .
186
        { ?type rdfs:subClassOf* skos:Concept . } UNION { ?type rdfs:subClassOf* skos:Collection . } $optional }
187
        OPTIONAL { ?type rdfs:label ?typelabel . }
188
      }
189
GROUP BY ?type ?typelabel
190
EOQ;
191
        return $query;
192
    }
193
194
    /**
195
     * Used for transforming the concept count query results.
196
     * @param EasyRdf\Sparql\Result $result query results to be transformed
197
     * @param string $lang language of labels
198
     * @return Array containing the label counts
199
     */
200
    private function transformCountConceptsResults($result, $lang) {
201
        $ret = array();
202
        foreach ($result as $row) {
203
            if (!isset($row->type)) {
204
                continue;
205
            }
206
            $ret[$row->type->getUri()]['type'] = $row->type->getUri();
207
            $ret[$row->type->getUri()]['count'] = $row->c->getValue();
208
            if (isset($row->typelabel) && $row->typelabel->getLang() === $lang) {
209
                $ret[$row->type->getUri()]['label'] = $row->typelabel->getValue();
210
            }
211
212
        }
213
        return $ret;
214
    }
215
216
    /**
217
     * Used for counting number of concepts and collections in a vocabulary.
218
     * @param string $lang language of labels
219
     * @return int number of concepts in this vocabulary
220
     */
221
    public function countConcepts($lang = null, $array = null, $group = null) {
222
        $query = $this->generateCountConceptsQuery($array, $group);
223
        $result = $this->query($query);
224
        return $this->transformCountConceptsResults($result, $lang);
0 ignored issues
show
Bug introduced by
It seems like $result defined by $this->query($query) on line 223 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...
225
    }
226
227
    /**
228
     * @param array $langs Languages to query for
229
     * @param string[] $props property names
230
     * @return string sparql query
231
     */
232
    private function generateCountLangConceptsQuery($langs, $classes, $props) {
233
        $gcl = $this->graphClause;
234
        $classes = ($classes) ? $classes : array('http://www.w3.org/2004/02/skos/core#Concept');
235
236
	$quote_string = function($val) { return "'$val'"; };
237
	$quoted_values = array_map($quote_string, $langs);
238
	$langFilter = "FILTER(?lang IN (" . implode(',', $quoted_values) . "))";
239
240
        $values = $this->formatValues('?type', $classes, 'uri');
241
        $valuesProp = $this->formatValues('?prop', $props, null);
242
243
        $query = <<<EOQ
244
SELECT ?lang ?prop
245
  (COUNT(?label) as ?count)
246
WHERE {
247
  $gcl {
248
    $values
249
    $valuesProp
250
    ?conc a ?type .
251
    ?conc ?prop ?label .
252
    BIND(LANG(?label) AS ?lang)
253
    $langFilter
254
  }
255
}
256
GROUP BY ?lang ?prop ?type
257
EOQ;
258
        return $query;
259
    }
260
261
    /**
262
     * Transforms the CountLangConcepts results into an array of label counts.
263
     * @param EasyRdf\Sparql\Result $result query results to be transformed
264
     * @param array $langs Languages to query for
265
     * @param string[] $props property names
266
     */
267
    private function transformCountLangConceptsResults($result, $langs, $props) {
268
        $ret = array();
269
        // set default count to zero; overridden below if query found labels
270
        foreach ($langs as $lang) {
271
            foreach ($props as $prop) {
272
                $ret[$lang][$prop] = 0;
273
            }
274
        }
275
        foreach ($result as $row) {
276
            if (isset($row->lang) && isset($row->prop) && isset($row->count)) {
277
                $ret[$row->lang->getValue()][$row->prop->shorten()] +=
278
                $row->count->getValue();
279
            }
280
281
        }
282
        ksort($ret);
283
        return $ret;
284
    }
285
286
    /**
287
     * Counts the number of concepts in a easyRDF graph with a specific language.
288
     * @param array $langs Languages to query for
289
     * @return Array containing count of concepts for each language and property.
290
     */
291
    public function countLangConcepts($langs, $classes = null) {
292
        $props = array('skos:prefLabel', 'skos:altLabel', 'skos:hiddenLabel');
293
        $query = $this->generateCountLangConceptsQuery($langs, $classes, $props);
294
        // Count the number of terms in each language
295
        $result = $this->query($query);
296
        return $this->transformCountLangConceptsResults($result, $langs, $props);
0 ignored issues
show
Bug introduced by
It seems like $result defined by $this->query($query) on line 295 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...
297
    }
298
299
    /**
300
     * Formats a VALUES clause (SPARQL 1.1) which states that the variable should be bound to one
301
     * of the constants given.
302
     * @param string $varname variable name, e.g. "?uri"
303
     * @param array $values the values
304
     * @param string $type type of values: "uri", "literal" or null (determines quoting style)
305
     */
306
    protected function formatValues($varname, $values, $type = null) {
307
        $constants = array();
308
        foreach ($values as $val) {
309
            if ($type == 'uri') {
310
                $val = "<$val>";
311
            }
312
313
            if ($type == 'literal') {
314
                $val = "'$val'";
315
            }
316
317
            $constants[] = "($val)";
318
        }
319
        $values = implode(" ", $constants);
320
321
        return "VALUES ($varname) { $values }";
322
    }
323
324
    /**
325
     * Filters multiple instances of the same vocabulary from the input array.
326
     * @param \Vocabulary[]|null $vocabs array of Vocabulary objects
327
     * @return \Vocabulary[]
328
     */
329
    private function filterDuplicateVocabs($vocabs) {
330
        // filtering duplicates
331
        $uniqueVocabs = array();
332
        if ($vocabs !== null && sizeof($vocabs) > 0) {
333
            foreach ($vocabs as $voc) {
334
                $uniqueVocabs[$voc->getId()] = $voc;
335
            }
336
        }
337
338
        return $uniqueVocabs;
339
    }
340
341
    /**
342
     * Generates a sparql query for one or more concept URIs
343
     * @param mixed $uris concept URI (string) or array of URIs
344
     * @param string|null $arrayClass the URI for thesaurus array class, or null if not used
345
     * @param \Vocabulary[]|null $vocabs array of Vocabulary objects
346
     * @return string sparql query
347
     */
348
    private function generateConceptInfoQuery($uris, $arrayClass, $vocabs) {
349
        $gcl = $this->graphClause;
350
        $fcl = empty($vocabs) ? '' : $this->generateFromClause($vocabs);
351
        $values = $this->formatValues('?uri', $uris, 'uri');
352
        $uniqueVocabs = $this->filterDuplicateVocabs($vocabs);
353
        $valuesGraph = empty($vocabs) ? $this->formatValuesGraph($uniqueVocabs) : '';
354
355
        if ($arrayClass === null) {
356
            $construct = $optional = "";
357
        } else {
358
            // add information that can be used to format narrower concepts by
359
            // the array they belong to ("milk by source animal" use case)
360
            $construct = "\n ?x skos:member ?o . ?x skos:prefLabel ?xl . ?x a <$arrayClass> .";
361
            $optional = "\n OPTIONAL {
362
                      ?x skos:member ?o .
363
                      ?x a <$arrayClass> .
364
                      ?x skos:prefLabel ?xl .
365
                      FILTER NOT EXISTS {
366
                        ?x skos:member ?other .
367
                        MINUS { ?other skos:broader ?uri }
368
                      }
369
                    }";
370
        }
371
        $query = <<<EOQ
372
CONSTRUCT {
373
 ?s ?p ?uri .
374
 ?sp ?uri ?op .
375
 ?uri ?p ?o .
376
 ?p rdfs:label ?proplabel .
377
 ?p rdfs:subPropertyOf ?pp .
378
 ?pp rdfs:label ?plabel .
379
 ?o a ?ot .
380
 ?o skos:prefLabel ?opl .
381
 ?o rdfs:label ?ol .
382
 ?o rdf:value ?ov .
383
 ?o skos:notation ?on .
384
 ?o ?oprop ?oval .
385
 ?o ?xlprop ?xlval .
386
 ?directgroup skos:member ?uri .
387
 ?parent skos:member ?group .
388
 ?group skos:prefLabel ?grouplabel .
389
 ?b1 rdf:first ?item .
390
 ?b1 rdf:rest ?b2 .
391
 ?item a ?it .
392
 ?item skos:prefLabel ?il .
393
 ?group a ?grouptype . $construct
394
} $fcl WHERE {
395
 $values
396
 $gcl {
397
  {
398
    ?s ?p ?uri .
399
    FILTER(!isBlank(?s))
400
    FILTER(?p != skos:inScheme)
401
  }
402
  UNION
403
  { ?sp ?uri ?op . }
404
  UNION
405
  {
406
    ?directgroup skos:member ?uri .
407
    ?group skos:member+ ?uri .
408
    ?group skos:prefLabel ?grouplabel .
409
    ?group a ?grouptype .
410
    OPTIONAL { ?parent skos:member ?group }
411
  }
412
  UNION
413
  {
414
   ?uri ?p ?o .
415
   OPTIONAL {
416
     ?o rdf:rest* ?b1 .
417
     ?b1 rdf:first ?item .
418
     ?b1 rdf:rest ?b2 .
419
     OPTIONAL { ?item a ?it . }
420
     OPTIONAL { ?item skos:prefLabel ?il . }
421
   }
422
   OPTIONAL {
423
     { ?p rdfs:label ?proplabel . }
424
     UNION
425
     { ?p rdfs:subPropertyOf ?pp . }
426
   }
427
   OPTIONAL {
428
     { ?o a ?ot . }
429
     UNION
430
     { ?o skos:prefLabel ?opl . }
431
     UNION
432
     { ?o rdfs:label ?ol . }
433
     UNION
434
     { ?o rdf:value ?ov . 
435
       OPTIONAL { ?o ?oprop ?oval . }
436
     }
437
     UNION
438
     { ?o skos:notation ?on . }
439
     UNION
440
     { ?o a skosxl:Label .
441
       ?o ?xlprop ?xlval }
442
   } $optional
443
  }
444
 }
445
}
446
$valuesGraph
447
EOQ;
448
        return $query;
449
    }
450
451
    /**
452
     * Transforms ConceptInfo query results into an array of Concept objects
453
     * @param EasyRdf\Graph $result query results to be transformed
454
     * @param array $uris concept URIs
455
     * @param \Vocabulary[] $vocabs array of Vocabulary object
456
     * @param string|null $clang content language
457
     * @return Concept[] array of Concept objects
458
     */
459
    private function transformConceptInfoResults($result, $uris, $vocabs, $clang) {
460
        $conceptArray = array();
461
        foreach ($uris as $index => $uri) {
462
            $conc = $result->resource($uri);
463
            $vocab = (isset($vocabs) && sizeof($vocabs) == 1) ? $vocabs[0] : $vocabs[$index];
464
            $conceptArray[] = new Concept($this->model, $vocab, $conc, $result, $clang);
465
        }
466
        return $conceptArray;
467
    }
468
469
    /**
470
     * Returns information (as a graph) for one or more concept URIs
471
     * @param mixed $uris concept URI (string) or array of URIs
472
     * @param string|null $arrayClass the URI for thesaurus array class, or null if not used
473
     * @param \Vocabulary[]|null $vocabs vocabularies to target
474
     * @return \EasyRdf\Graph
475
     */
476
    public function queryConceptInfoGraph($uris, $arrayClass = null, $vocabs = array()) {
477
        // if just a single URI is given, put it in an array regardless
478
        if (!is_array($uris)) {
479
            $uris = array($uris);
480
        }
481
482
        $query = $this->generateConceptInfoQuery($uris, $arrayClass, $vocabs);
483
        $result = $this->query($query);
484
        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...
485
    }
486
487
    /**
488
     * Returns information (as an array of Concept objects) for one or more concept URIs
489
     * @param mixed $uris concept URI (string) or array of URIs
490
     * @param string|null $arrayClass the URI for thesaurus array class, or null if not used
491
     * @param \Vocabulary[] $vocabs vocabularies to target
492
     * @param string|null $clang content language
493
     * @return Concept[]
494
     */
495
    public function queryConceptInfo($uris, $arrayClass = null, $vocabs = array(), $clang = null) {
496
        // if just a single URI is given, put it in an array regardless
497
        if (!is_array($uris)) {
498
            $uris = array($uris);
499
        }
500
        $result = $this->queryConceptInfoGraph($uris, $arrayClass, $vocabs);
501
        if ($result->isEmpty()) {
502
            return [];
503
        }
504
        return $this->transformConceptInfoResults($result, $uris, $vocabs, $clang);
505
    }
506
507
    /**
508
     * Generates the sparql query for queryTypes
509
     * @param string $lang
510
     * @return string sparql query
511
     */
512
    private function generateQueryTypesQuery($lang) {
513
        $fcl = $this->generateFromClause();
514
        $query = <<<EOQ
515
SELECT DISTINCT ?type ?label ?superclass $fcl
516
WHERE {
517
  {
518
    { BIND( skos:Concept as ?type ) }
519
    UNION
520
    { BIND( skos:Collection as ?type ) }
521
    UNION
522
    { BIND( isothes:ConceptGroup as ?type ) }
523
    UNION
524
    { BIND( isothes:ThesaurusArray as ?type ) }
525
    UNION
526
    { ?type rdfs:subClassOf/rdfs:subClassOf* skos:Concept . }
527
    UNION
528
    { ?type rdfs:subClassOf/rdfs:subClassOf* skos:Collection . }
529
  }
530
  OPTIONAL {
531
    ?type rdfs:label ?label .
532
    FILTER(langMatches(lang(?label), '$lang'))
533
  }
534
  OPTIONAL {
535
    ?type rdfs:subClassOf ?superclass .
536
  }
537
  FILTER EXISTS {
538
    ?s a ?type .
539
    ?s skos:prefLabel ?prefLabel .
540
  }
541
}
542
EOQ;
543
        return $query;
544
    }
545
546
    /**
547
     * Transforms the results into an array format.
548
     * @param EasyRdf\Sparql\Result $result
549
     * @return array Array with URIs (string) as key and array of (label, superclassURI) as value
550
     */
551 View Code Duplication
    private function transformQueryTypesResults($result) {
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...
552
        $ret = array();
553
        foreach ($result as $row) {
554
            $type = array();
555
            if (isset($row->label)) {
556
                $type['label'] = $row->label->getValue();
557
            }
558
559
            if (isset($row->superclass)) {
560
                $type['superclass'] = $row->superclass->getUri();
561
            }
562
563
            $ret[$row->type->getURI()] = $type;
564
        }
565
        return $ret;
566
    }
567
568
    /**
569
     * Retrieve information about types from the endpoint
570
     * @param string $lang
571
     * @return array Array with URIs (string) as key and array of (label, superclassURI) as value
572
     */
573
    public function queryTypes($lang) {
574
        $query = $this->generateQueryTypesQuery($lang);
575
        $result = $this->query($query);
576
        return $this->transformQueryTypesResults($result);
0 ignored issues
show
Bug introduced by
It seems like $result defined by $this->query($query) on line 575 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...
577
    }
578
579
    /**
580
     * Generates the concept scheme query.
581
     * @param string $conceptscheme concept scheme URI
582
     * @return string sparql query
583
     */
584
    private function generateQueryConceptSchemeQuery($conceptscheme) {
585
        $fcl = $this->generateFromClause();
586
        $query = <<<EOQ
587
CONSTRUCT {
588
  <$conceptscheme> ?property ?value .
589
} $fcl WHERE {
590
  <$conceptscheme> ?property ?value .
591
  FILTER (?property != skos:hasTopConcept)
592
}
593
EOQ;
594
        return $query;
595
    }
596
597
    /**
598
     * Retrieves conceptScheme information from the endpoint.
599
     * @param string $conceptscheme concept scheme URI
600
     * @return EasyRDF_Graph query result graph
601
     */
602
    public function queryConceptScheme($conceptscheme) {
603
        $query = $this->generateQueryConceptSchemeQuery($conceptscheme);
604
        return $this->query($query);
605
    }
606
607
    /**
608
     * Generates the queryConceptSchemes sparql query.
609
     * @param string $lang language of labels
610
     * @return string sparql query
611
     */
612
    private function generateQueryConceptSchemesQuery($lang) {
613
        $fcl = $this->generateFromClause();
614
        $query = <<<EOQ
615
SELECT ?cs ?label ?preflabel ?title ?domain ?domainLabel $fcl
616
WHERE {
617
 ?cs a skos:ConceptScheme .
618
 OPTIONAL{
619
    ?cs dcterms:subject ?domain.
620
    ?domain skos:prefLabel ?domainLabel.
621
    FILTER(langMatches(lang(?domainLabel), '$lang'))
622
}
623
 OPTIONAL {
624
   ?cs rdfs:label ?label .
625
   FILTER(langMatches(lang(?label), '$lang'))
626
 }
627
 OPTIONAL {
628
   ?cs skos:prefLabel ?preflabel .
629
   FILTER(langMatches(lang(?preflabel), '$lang'))
630
 }
631
 OPTIONAL {
632
   { ?cs dc11:title ?title }
633
   UNION
634
   { ?cs dc:title ?title }
635
   FILTER(langMatches(lang(?title), '$lang'))
636
 }
637
} 
638
ORDER BY ?cs
639
EOQ;
640
        return $query;
641
    }
642
643
    /**
644
     * Transforms the queryConceptScheme results into an array format.
645
     * @param EasyRdf\Sparql\Result $result
646
     * @return array
647
     */
648
    private function transformQueryConceptSchemesResults($result) {
649
        $ret = array();
650
        foreach ($result as $row) {
651
            $conceptscheme = array();
652
            if (isset($row->label)) {
653
                $conceptscheme['label'] = $row->label->getValue();
654
            }
655
656
            if (isset($row->preflabel)) {
657
                $conceptscheme['prefLabel'] = $row->preflabel->getValue();
658
            }
659
660
            if (isset($row->title)) {
661
                $conceptscheme['title'] = $row->title->getValue();
662
            }
663
            // add dct:subject and their labels in the result
664
            if(isset($row->domain) && isset($row->domainLabel)){
665
                $conceptscheme['subject']['uri']=$row->domain->getURI();
666
                $conceptscheme['subject']['prefLabel']=$row->domainLabel->getValue();
667
            }
668
669
            $ret[$row->cs->getURI()] = $conceptscheme;
670
        }
671
        return $ret;
672
    }
673
674
    /**
675
     * return a list of skos:ConceptScheme instances in the given graph
676
     * @param string $lang language of labels
677
     * @return array Array with concept scheme URIs (string) as keys and labels (string) as values
678
     */
679
    public function queryConceptSchemes($lang) {
680
        $query = $this->generateQueryConceptSchemesQuery($lang);
681
        $result = $this->query($query);
682
        return $this->transformQueryConceptSchemesResults($result);
0 ignored issues
show
Bug introduced by
It seems like $result defined by $this->query($query) on line 681 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...
683
    }
684
685
    /**
686
     * Generate a VALUES clause for limiting the targeted graphs.
687
     * @param Vocabulary[]|null $vocabs the vocabularies to target
688
     * @return string[] array of graph URIs
689
     */
690
    protected function getVocabGraphs($vocabs) {
691
        if ($vocabs === null || sizeof($vocabs) == 0) {
692
            // searching from all vocabularies - limit to known graphs
693
            $vocabs = $this->model->getVocabularies();
694
        }
695
        $graphs = array();
696
        foreach ($vocabs as $voc) {
697
            $graph = $voc->getGraph();
698
            if (!is_null($graph) && !in_array($graph, $graphs)) {
699
                $graphs[] = $graph;
700
            }
701
        }
702
        return $graphs;
703
    }
704
705
    /**
706
     * Generate a VALUES clause for limiting the targeted graphs.
707
     * @param Vocabulary[]|null $vocabs array of Vocabulary objects to target
708
     * @return string VALUES clause, or "" if not necessary to limit
709
     */
710
    protected function formatValuesGraph($vocabs) {
711
        if (!$this->isDefaultEndpoint()) {
712
            return "";
713
        }
714
        $graphs = $this->getVocabGraphs($vocabs);
715
        return $this->formatValues('?graph', $graphs, 'uri');
716
    }
717
718
    /**
719
     * Generate a FILTER clause for limiting the targeted graphs.
720
     * @param array $vocabs array of Vocabulary objects to target
721
     * @return string FILTER clause, or "" if not necessary to limit
722
     */
723
    protected function formatFilterGraph($vocabs) {
724
        if (!$this->isDefaultEndpoint()) {
725
            return "";
726
        }
727
        $graphs = $this->getVocabGraphs($vocabs);
728
        $values = array();
729
        foreach ($graphs as $graph) {
730
          $values[] = "<$graph>";
731
        }
732
        if (count($values)) {
733
          return "FILTER (?graph IN (" . implode(',', $values) . "))";
734
        }
735
    }
736
737
    /**
738
     * Formats combined limit and offset clauses for the sparql query
739
     * @param int $limit maximum number of hits to retrieve; 0 for unlimited
740
     * @param int $offset offset of results to retrieve; 0 for beginning of list
741
     * @return string sparql query clauses
742
     */
743
    protected function formatLimitAndOffset($limit, $offset) {
744
        $limit = ($limit) ? 'LIMIT ' . $limit : '';
745
        $offset = ($offset) ? 'OFFSET ' . $offset : '';
746
        // eliminating whitespace and line changes when the conditions aren't needed.
747
        $limitandoffset = '';
748
        if ($limit && $offset) {
749
            $limitandoffset = "\n" . $limit . "\n" . $offset;
750
        } elseif ($limit) {
751
            $limitandoffset = "\n" . $limit;
752
        } elseif ($offset) {
753
            $limitandoffset = "\n" . $offset;
754
        }
755
756
        return $limitandoffset;
757
    }
758
759
    /**
760
     * Formats a sparql query clause for limiting the search to specific concept types.
761
     * @param array $types limit search to concepts of the given type(s)
762
     * @return string sparql query clause
763
     */
764
    protected function formatTypes($types) {
765
        $typePatterns = array();
766
        if (!empty($types)) {
767
            foreach ($types as $type) {
768
                $unprefixed = EasyRdf\RdfNamespace::expand($type);
769
                $typePatterns[] = "{ ?s a <$unprefixed> }";
770
            }
771
        }
772
773
        return implode(' UNION ', $typePatterns);
774
    }
775
776
    /**
777
     * @param string $prop property to include in the result eg. 'broader' or 'narrower'
778
     * @return string sparql query clause
779
     */
780
    private function formatPropertyCsvClause($prop) {
781
        # This expression creates a CSV row containing pairs of (uri,prefLabel) values.
782
        # The REPLACE is performed for quotes (" -> "") so they don't break the CSV format.
783
        $clause = <<<EOV
784
(GROUP_CONCAT(DISTINCT CONCAT(
785
 '"', IF(isIRI(?$prop),STR(?$prop),''), '"', ',',
786
 '"', REPLACE(IF(BOUND(?{$prop}lab),?{$prop}lab,''), '"', '""'), '"', ',',
787
 '"', REPLACE(IF(isLiteral(?{$prop}),?{$prop},''), '"', '""'), '"'
788
); separator='\\n') as ?{$prop}s)
789
EOV;
790
        return $clause;
791
    }
792
793
    /**
794
     * @return string sparql query clause
795
     */
796
    private function formatPrefLabelCsvClause() {
797
        # This expression creates a CSV row containing pairs of (prefLabel, lang) values.
798
        # The REPLACE is performed for quotes (" -> "") so they don't break the CSV format.
799
        $clause = <<<EOV
800
(GROUP_CONCAT(DISTINCT CONCAT(
801
 '"', STR(?pref), '"', ',', '"', lang(?pref), '"'
802
); separator='\\n') as ?preflabels)
803
EOV;
804
        return $clause;
805
    }
806
807
    /**
808
     * @param string $lang language code of the returned labels
809
     * @param array|null $fields extra fields to include in the result (array of strings). (default: null = none)
810
     * @return string sparql query clause
811
     */
812
    protected function formatExtraFields($lang, $fields) {
813
        // extra variable expressions to request and extra fields to query for
814
        $ret = array('extravars' => '', 'extrafields' => '');
815
816
        if ($fields === null) {
817
            return $ret;
818
        }
819
820
        if (in_array('prefLabel', $fields)) {
821
            $ret['extravars'] .= $this->formatPreflabelCsvClause();
822
            $ret['extrafields'] .= <<<EOF
823
OPTIONAL {
824
  ?s skos:prefLabel ?pref .
825
}
826
EOF;
827
            // removing the prefLabel from the fields since it has been handled separately
828
            $fields = array_diff($fields, array('prefLabel'));
829
        }
830
831
        foreach ($fields as $field) {
832
            $ret['extravars'] .= $this->formatPropertyCsvClause($field);
833
            $ret['extrafields'] .= <<<EOF
834
OPTIONAL {
835
  ?s skos:$field ?$field .
836
  FILTER(!isLiteral(?$field)||langMatches(lang(?{$field}), '$lang'))
837
  OPTIONAL { ?$field skos:prefLabel ?{$field}lab . FILTER(langMatches(lang(?{$field}lab), '$lang')) }
838
}
839
EOF;
840
        }
841
842
        return $ret;
843
    }
844
845
    /**
846
     * Generate condition for matching labels in SPARQL
847
     * @param string $term search term
848
     * @param string $searchLang language code used for matching labels (null means any language)
849
     * @return string sparql query snippet
850
     */
851
    protected function generateConceptSearchQueryCondition($term, $searchLang)
852
    {
853
        # use appropriate matching function depending on query type: =, strstarts, strends or full regex
854
        if (preg_match('/^[^\*]+$/', $term)) { // exact query
855
            $term = str_replace('\\', '\\\\', $term); // quote slashes
856
            $term = str_replace('\'', '\\\'', mb_strtolower($term, 'UTF-8')); // make lowercase and escape single quotes
857
            $filtercond = "LCASE(STR(?match)) = '$term'";
858
        } elseif (preg_match('/^[^\*]+\*$/', $term)) { // prefix query
859
            $term = substr($term, 0, -1); // remove the final asterisk
860
            $term = str_replace('\\', '\\\\', $term); // quote slashes
861
            $term = str_replace('\'', '\\\'', mb_strtolower($term, 'UTF-8')); // make lowercase and escape single quotes
862
            $filtercond = "STRSTARTS(LCASE(STR(?match)), '$term')";
863
        } elseif (preg_match('/^\*[^\*]+$/', $term)) { // suffix query
864
            $term = substr($term, 1); // remove the preceding asterisk
865
            $term = str_replace('\\', '\\\\', $term); // quote slashes
866
            $term = str_replace('\'', '\\\'', mb_strtolower($term, 'UTF-8')); // make lowercase and escape single quotes
867
            $filtercond = "STRENDS(LCASE(STR(?match)), '$term')";
868
        } else { // too complicated - have to use a regex
869
            # make sure regex metacharacters are not passed through
870
            $term = str_replace('\\', '\\\\', preg_quote($term));
871
            $term = str_replace('\\\\*', '.*', $term); // convert asterisk to regex syntax
872
            $term = str_replace('\'', '\\\'', $term); // ensure single quotes are quoted
873
            $filtercond = "REGEX(STR(?match), '^$term$', 'i')";
874
        }
875
876
        $labelcondMatch = ($searchLang) ? "&& (?prop = skos:notation || LANGMATCHES(lang(?match), ?langParam))" : "";
877
878
        return "?s ?prop ?match . FILTER ($filtercond $labelcondMatch)";
879
    }
880
881
882
    /**
883
     * Inner query for concepts using a search term.
884
     * @param string $term search term
885
     * @param string $lang language code of the returned labels
886
     * @param string $searchLang language code used for matching labels (null means any language)
887
     * @param string[] $props properties to target e.g. array('skos:prefLabel','skos:altLabel')
888
     * @param boolean $unique restrict results to unique concepts (default: false)
889
     * @return string sparql query
890
     */
891
    protected function generateConceptSearchQueryInner($term, $lang, $searchLang, $props, $unique, $filterGraph)
892
    {
893
        $valuesProp = $this->formatValues('?prop', $props);
894
        $textcond = $this->generateConceptSearchQueryCondition($term, $searchLang);
895
896
        $rawterm = str_replace(array('\\', '*', '"'), array('\\\\', '', '\"'), $term);
897
        // graph clause, if necessary
898
        $graphClause = $filterGraph != '' ? 'GRAPH ?graph' : '';
899
900
        // extra conditions for label language, if specified
901
        $labelcondLabel = ($lang) ? "LANGMATCHES(lang(?label), '$lang')" : "lang(?match) = '' || LANGMATCHES(lang(?label), lang(?match))";
902
        // if search language and UI/display language differ, must also consider case where there is no prefLabel in
903
        // the display language; in that case, should use the label with the same language as the matched label
904
        $labelcondFallback = ($searchLang != $lang) ?
905
          "OPTIONAL { # in case previous OPTIONAL block gives no labels\n" .
906
          "?s skos:prefLabel ?label . FILTER (LANGMATCHES(LANG(?label), LANG(?match))) }" : "";
907
908
        //  Including the labels if there is no query term given.
909
        if ($rawterm === '') {
910
          $labelClause = "?s skos:prefLabel ?label .";
911
          $labelClause = ($lang) ? $labelClause . " FILTER (LANGMATCHES(LANG(?label), '$lang'))" : $labelClause . "";
912
          return $labelClause . " BIND(?label AS ?match)";
913
        }
914
915
        /*
916
         * This query does some tricks to obtain a list of unique concepts.
917
         * From each match generated by the text index, a string such as
918
         * "1en@example" is generated, where the first character is a number
919
         * encoding the property and priority, then comes the language tag and
920
         * finally the original literal after an @ sign. Of these, the MIN
921
         * function is used to pick the best match for each concept. Finally,
922
         * the structure is unpacked to get back the original string. Phew!
923
         */
924
        $hitvar = $unique ? '(MIN(?matchstr) AS ?hit)' : '(?matchstr AS ?hit)';
925
        $hitgroup = $unique ? 'GROUP BY ?s ?label ?notation' : '';
926
927
        $langClause = $this->generateLangClause($searchLang);
928
929
        $query = <<<EOQ
930
   SELECT DISTINCT ?s ?label ?notation $hitvar
931
   WHERE {
932
    $graphClause {
933
     { 
934
     $valuesProp
935
     VALUES (?prop ?pri ?langParam) { (skos:prefLabel 1 $langClause) (skos:altLabel 3 $langClause) (skos:notation 5 '') (skos:hiddenLabel 7 $langClause)}
936
     $textcond
937
     ?s ?prop ?match }
938
     OPTIONAL {
939
      ?s skos:prefLabel ?label .
940
      FILTER ($labelcondLabel)
941
     } $labelcondFallback
942
     BIND(IF(langMatches(LANG(?match),'$lang'), ?pri, ?pri+1) AS ?npri)
943
     BIND(CONCAT(STR(?npri), LANG(?match), '@', STR(?match)) AS ?matchstr)
944
     OPTIONAL { ?s skos:notation ?notation }
945
    }
946
    $filterGraph
947
   }
948
   $hitgroup
949
EOQ;
950
951
        return $query;
952
    }
953
    /**
954
    *  This function can be overwritten in other SPARQL dialects for the possibility of handling the differenc language clauses
955
     * @param string $lang
956
     * @return string formatted language clause
957
     */
958
    protected function generateLangClause($lang) {
959
        return "'$lang'";
960
    }
961
962
    /**
963
     * Query for concepts using a search term.
964
     * @param array|null $fields extra fields to include in the result (array of strings). (default: null = none)
965
     * @param boolean $unique restrict results to unique concepts (default: false)
966
     * @param boolean $showDeprecated whether to include deprecated concepts in search results (default: false)
967
     * @param ConceptSearchParameters $params
968
     * @return string sparql query
969
     */
970
    protected function generateConceptSearchQuery($fields, $unique, $params, $showDeprecated = false) {
971
        $vocabs = $params->getVocabs();
972
        $gcl = $this->graphClause;
973
        $fcl = empty($vocabs) ? '' : $this->generateFromClause($vocabs);
974
        $formattedtype = $this->formatTypes($params->getTypeLimit());
975
        $formattedfields = $this->formatExtraFields($params->getLang(), $fields);
976
        $extravars = $formattedfields['extravars'];
977
        $extrafields = $formattedfields['extrafields'];
978
        $schemes = $params->getSchemeLimit();
979
980
        // limit the search to only requested concept schemes
981
        $schemecond = '';
982
        if (!empty($schemes)) {
983
            $conditions = array();
984
            foreach($schemes as $scheme) {
985
                $conditions[] = "{?s skos:inScheme <$scheme>}";
986
            }
987
            $schemecond = '{'.implode(" UNION ",$conditions).'}';
988
        }
989
        $filterDeprecated="";
990
        //show or hide deprecated concepts
991
        if(!$showDeprecated){
992
            $filterDeprecated="FILTER NOT EXISTS { ?s owl:deprecated true }";
993
        }
994
        // extra conditions for parent and group, if specified
995
        $parentcond = ($params->getParentLimit()) ? "?s skos:broader+ <" . $params->getParentLimit() . "> ." : "";
996
        $groupcond = ($params->getGroupLimit()) ? "<" . $params->getGroupLimit() . "> skos:member ?s ." : "";
997
        $pgcond = $parentcond . $groupcond;
998
999
        $orderextra = $this->isDefaultEndpoint() ? $this->graph : '';
1000
1001
        # make VALUES clauses
1002
        $props = array('skos:prefLabel', 'skos:altLabel');
1003
1004
        //add notation into searchable data for the vocabularies which have been configured for it
1005
        if ($vocabs) {
1006
            $searchByNotation = false;
1007
            foreach ($vocabs as $vocab) {
1008
                if ($vocab->getConfig()->searchByNotation()) {
1009
                    $searchByNotation = true;
1010
                }
1011
            }
1012
            if ($searchByNotation) {
1013
                $props[] = 'skos:notation';
1014
            }
1015
        }
1016
1017
        if ($params->getHidden()) {
1018
            $props[] = 'skos:hiddenLabel';
1019
        }
1020
        $filterGraph = empty($vocabs) ? $this->formatFilterGraph($vocabs) : '';
1021
1022
        // remove futile asterisks from the search term
1023
        $term = $params->getSearchTerm();
1024
        while (strpos($term, '**') !== false) {
1025
            $term = str_replace('**', '*', $term);
1026
        }
1027
1028
        $labelpriority = <<<EOQ
1029
  FILTER(BOUND(?s))
1030
  BIND(STR(SUBSTR(?hit,1,1)) AS ?pri)
1031
  BIND(IF((SUBSTR(STRBEFORE(?hit, '@'),1) != ?pri), STRLANG(STRAFTER(?hit, '@'), SUBSTR(STRBEFORE(?hit, '@'),2)), STRAFTER(?hit, '@')) AS ?match)
1032
  BIND(IF((?pri = "1" || ?pri = "2") && ?match != ?label, ?match, ?unbound) as ?plabel)
1033
  BIND(IF((?pri = "3" || ?pri = "4"), ?match, ?unbound) as ?alabel)
1034
  BIND(IF((?pri = "7" || ?pri = "8"), ?match, ?unbound) as ?hlabel)
1035
EOQ;
1036
        $innerquery = $this->generateConceptSearchQueryInner($params->getSearchTerm(), $params->getLang(), $params->getSearchLang(), $props, $unique, $filterGraph);
1037
        if ($params->getSearchTerm() === '*' || $params->getSearchTerm() === '') {
1038
          $labelpriority = '';
1039
        }
1040
        $query = <<<EOQ
1041
SELECT DISTINCT ?s ?label ?plabel ?alabel ?hlabel ?graph ?notation (GROUP_CONCAT(DISTINCT STR(?type);separator=' ') as ?types) $extravars 
1042
$fcl
1043
WHERE {
1044
 $gcl {
1045
  {
1046
  $innerquery
1047
  }
1048
  $labelpriority
1049
  $formattedtype
1050
  { $pgcond 
1051
   ?s a ?type .
1052
   $extrafields $schemecond
1053
  }
1054
  $filterDeprecated
1055
 }
1056
 $filterGraph
1057
}
1058
GROUP BY ?s ?match ?label ?plabel ?alabel ?hlabel ?notation ?graph
1059
ORDER BY LCASE(STR(?match)) LANG(?match) $orderextra
1060
EOQ;
1061
        return $query;
1062
    }
1063
1064
    /**
1065
     * Transform a single concept search query results into the skosmos desired return format.
1066
     * @param $row SPARQL query result row
1067
     * @param array $vocabs array of Vocabulary objects to search; empty for global search
1068
     * @return array query result object
1069
     */
1070
    private function transformConceptSearchResult($row, $vocabs, $fields)
1071
    {
1072
        $hit = array();
1073
        $hit['uri'] = $row->s->getUri();
1074
1075
        if (isset($row->graph)) {
1076
            $hit['graph'] = $row->graph->getUri();
1077
        }
1078
1079
        foreach (explode(" ", $row->types->getValue()) as $typeuri) {
1080
            $hit['type'][] = $this->shortenUri($typeuri);
1081
        }
1082
1083
        if(!empty($fields)) {
1084
            foreach ($fields as $prop) {
1085
                $propname = $prop . 's';
1086
                if (isset($row->$propname)) {
1087
                    foreach (explode("\n", $row->$propname->getValue()) as $line) {
1088
                        $rdata = str_getcsv($line, ',', '"', '"');
1089
                        $propvals = array();
1090
                        if ($rdata[0] != '') {
1091
                            $propvals['uri'] = $rdata[0];
1092
                        }
1093
                        if ($rdata[1] != '') {
1094
                            $propvals['prefLabel'] = $rdata[1];
1095
                        }
1096
                        if ($rdata[2] != '') {
1097
                            $propvals = $rdata[2];
1098
                        }
1099
1100
                        $hit['skos:' . $prop][] = $propvals;
1101
                    }
1102
                }
1103
            }
1104
        }
1105
1106
1107
        if (isset($row->preflabels)) {
1108
            foreach (explode("\n", $row->preflabels->getValue()) as $line) {
1109
                $pref = str_getcsv($line, ',', '"', '"');
1110
                $hit['prefLabels'][$pref[1]] = $pref[0];
1111
            }
1112
        }
1113
1114
        foreach ($vocabs as $vocab) { // looping the vocabulary objects and asking these for a localname for the concept.
1115
            $localname = $vocab->getLocalName($hit['uri']);
1116
            if ($localname !== $hit['uri']) { // only passing the result forward if the uri didn't boomerang right back.
1117
                $hit['localname'] = $localname;
1118
                break; // stopping the search when we find one that returns something valid.
1119
            }
1120
        }
1121
1122
        if (isset($row->label)) {
1123
            $hit['prefLabel'] = $row->label->getValue();
1124
        }
1125
1126
        if (isset($row->label)) {
1127
            $hit['lang'] = $row->label->getLang();
1128
        }
1129
1130
        if (isset($row->notation)) {
1131
            $hit['notation'] = $row->notation->getValue();
1132
        }
1133
1134
        if (isset($row->plabel)) {
1135
            $hit['matchedPrefLabel'] = $row->plabel->getValue();
1136
            $hit['lang'] = $row->plabel->getLang();
1137 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...
1138
            $hit['altLabel'] = $row->alabel->getValue();
1139
            $hit['lang'] = $row->alabel->getLang();
1140
        } elseif (isset($row->hlabel)) {
1141
            $hit['hiddenLabel'] = $row->hlabel->getValue();
1142
            $hit['lang'] = $row->hlabel->getLang();
1143
        }
1144
        return $hit;
1145
    }
1146
1147
    /**
1148
     * Transform the concept search query results into the skosmos desired return format.
1149
     * @param EasyRdf\Sparql\Result $results
1150
     * @param array $vocabs array of Vocabulary objects to search; empty for global search
1151
     * @return array query result object
1152
     */
1153
    private function transformConceptSearchResults($results, $vocabs, $fields) {
1154
        $ret = array();
1155
1156
        foreach ($results as $row) {
1157
            if (!isset($row->s)) {
1158
                // don't break if query returns a single dummy result
1159
                continue;
1160
            }
1161
            $ret[] = $this->transformConceptSearchResult($row, $vocabs, $fields);
1162
        }
1163
        return $ret;
1164
    }
1165
1166
    /**
1167
     * Query for concepts using a search term.
1168
     * @param array $vocabs array of Vocabulary objects to search; empty for global search
1169
     * @param array $fields extra fields to include in the result (array of strings). (default: null = none)
1170
     * @param boolean $unique restrict results to unique concepts (default: false)
1171
     * @param boolean $showDeprecated whether to include deprecated concepts in the result (default: false)
1172
     * @param ConceptSearchParameters $params
1173
     * @return array query result object
1174
     */
1175
    public function queryConcepts($vocabs, $fields = null, $unique = false, $params, $showDeprecated = false) {
1176
        $query = $this->generateConceptSearchQuery($fields, $unique, $params,$showDeprecated);
1177
        $results = $this->query($query);
1178
        return $this->transformConceptSearchResults($results, $vocabs, $fields);
0 ignored issues
show
Bug introduced by
It seems like $results defined by $this->query($query) on line 1177 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...
1179
    }
1180
1181
    /**
1182
     * Generates sparql query clauses used for creating the alphabetical index.
1183
     * @param string $letter the letter (or special class) to search for
1184
     * @return array of sparql query clause strings
1185
     */
1186
    private function formatFilterConditions($letter, $lang) {
1187
        $useRegex = false;
1188
1189
        if ($letter == '*') {
1190
            $letter = '.*';
1191
            $useRegex = true;
1192
        } elseif ($letter == '0-9') {
1193
            $letter = '[0-9].*';
1194
            $useRegex = true;
1195
        } elseif ($letter == '!*') {
1196
            $letter = '[^\\\\p{L}\\\\p{N}].*';
1197
            $useRegex = true;
1198
        }
1199
1200
        # make text query clause
1201
        $lcletter = mb_strtolower($letter, 'UTF-8'); // convert to lower case, UTF-8 safe
1202
        if ($useRegex) {
1203
            $filtercondLabel = $lang ? "regex(str(?label), '^$letter$', 'i') && langMatches(lang(?label), '$lang')" : "regex(str(?label), '^$letter$', 'i')";
1204
            $filtercondALabel = $lang ? "regex(str(?alabel), '^$letter$', 'i') && langMatches(lang(?alabel), '$lang')" : "regex(str(?alabel), '^$letter$', 'i')";
1205
        } else {
1206
            $filtercondLabel = $lang ? "strstarts(lcase(str(?label)), '$lcletter') && langMatches(lang(?label), '$lang')" : "strstarts(lcase(str(?label)), '$lcletter')";
1207
            $filtercondALabel = $lang ? "strstarts(lcase(str(?alabel)), '$lcletter') && langMatches(lang(?alabel), '$lang')" : "strstarts(lcase(str(?alabel)), '$lcletter')";
1208
        }
1209
        return array('filterpref' => $filtercondLabel, 'filteralt' => $filtercondALabel);
1210
    }
1211
1212
    /**
1213
     * Generates the sparql query used for rendering the alphabetical index.
1214
     * @param string $letter the letter (or special class) to search for
1215
     * @param string $lang language of labels
1216
     * @param integer $limit limits the amount of results
1217
     * @param integer $offset offsets the result set
1218
     * @param array|null $classes
1219
     * @param boolean $showDeprecated whether to include deprecated concepts in the result (default: false)
1220
     * @param \EasyRdf\Resource|null $qualifier alphabetical list qualifier resource or null (default: null)
1221
     * @return string sparql query
1222
     */
1223
    protected function generateAlphabeticalListQuery($letter, $lang, $limit, $offset, $classes, $showDeprecated = false, $qualifier = null) {
1224
        $fcl = $this->generateFromClause();
1225
        $classes = ($classes) ? $classes : array('http://www.w3.org/2004/02/skos/core#Concept');
1226
        $values = $this->formatValues('?type', $classes, 'uri');
1227
        $limitandoffset = $this->formatLimitAndOffset($limit, $offset);
1228
        $conditions = $this->formatFilterConditions($letter, $lang);
1229
        $filtercondLabel = $conditions['filterpref'];
1230
        $filtercondALabel = $conditions['filteralt'];
1231
        $qualifierClause = $qualifier ? "OPTIONAL { ?s <" . $qualifier->getURI() . "> ?qualifier }" : "";
1232
        $filterDeprecated="";
1233
        if(!$showDeprecated){
1234
            $filterDeprecated="FILTER NOT EXISTS { ?s owl:deprecated true }";
1235
        }
1236
        $query = <<<EOQ
1237
SELECT DISTINCT ?s ?label ?alabel ?qualifier $fcl
1238
WHERE {
1239
  {
1240
    ?s skos:prefLabel ?label .
1241
    FILTER (
1242
      $filtercondLabel
1243
    )
1244
  }
1245
  UNION
1246
  {
1247
    {
1248
      ?s skos:altLabel ?alabel .
1249
      FILTER (
1250
        $filtercondALabel
1251
      )
1252
    }
1253
    {
1254
      ?s skos:prefLabel ?label .
1255
      FILTER (langMatches(lang(?label), '$lang'))
1256
    }
1257
  }
1258
  ?s a ?type .
1259
  $qualifierClause
1260
  $filterDeprecated
1261
  $values
1262
}
1263
ORDER BY LCASE(STR(COALESCE(?alabel, ?label))) STR(?s) LCASE(STR(?qualifier)) $limitandoffset
1264
EOQ;
1265
        return $query;
1266
    }
1267
1268
    /**
1269
     * Transforms the alphabetical list query results into an array format.
1270
     * @param EasyRdf\Sparql\Result $results
1271
     * @return array
1272
     */
1273
    private function transformAlphabeticalListResults($results) {
1274
        $ret = array();
1275
1276
        foreach ($results as $row) {
1277
            if (!isset($row->s)) {
1278
                continue;
1279
            }
1280
            // don't break if query returns a single dummy result
1281
1282
            $hit = array();
1283
            $hit['uri'] = $row->s->getUri();
1284
1285
            $hit['localname'] = $row->s->localName();
1286
1287
            $hit['prefLabel'] = $row->label->getValue();
1288
            $hit['lang'] = $row->label->getLang();
1289
1290 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...
1291
                $hit['altLabel'] = $row->alabel->getValue();
1292
                $hit['lang'] = $row->alabel->getLang();
1293
            }
1294
1295
            if (isset($row->qualifier)) {
1296
                if ($row->qualifier instanceof EasyRdf\Literal) {
1297
                    $hit['qualifier'] = $row->qualifier->getValue();
1298
                }
1299
                else {
1300
                    $hit['qualifier'] = $row->qualifier->localName();
1301
                }
1302
            }
1303
1304
            $ret[] = $hit;
1305
        }
1306
1307
        return $ret;
1308
    }
1309
1310
    /**
1311
     * Query for concepts with a term starting with the given letter. Also special classes '0-9' (digits),
1312
     * '*!' (special characters) and '*' (everything) are accepted.
1313
     * @param string $letter the letter (or special class) to search for
1314
     * @param string $lang language of labels
1315
     * @param integer $limit limits the amount of results
1316
     * @param integer $offset offsets the result set
1317
     * @param array $classes
1318
     * @param boolean $showDeprecated whether to include deprecated concepts in the result (default: false)
1319
     * @param \EasyRdf\Resource|null $qualifier alphabetical list qualifier resource or null (default: null)
1320
     */
1321
    public function queryConceptsAlphabetical($letter, $lang, $limit = null, $offset = null, $classes = null, $showDeprecated = false, $qualifier = null) {
1322
        $query = $this->generateAlphabeticalListQuery($letter, $lang, $limit, $offset, $classes, $showDeprecated, $qualifier);
1323
        $results = $this->query($query);
1324
        return $this->transformAlphabeticalListResults($results);
0 ignored issues
show
Bug introduced by
It seems like $results defined by $this->query($query) on line 1323 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...
1325
    }
1326
1327
    /**
1328
     * Creates the query used for finding out which letters should be displayed in the alphabetical index.
1329
     * Note that we force the datatype of the result variable otherwise Virtuoso does not properly interpret the DISTINCT and we have duplicated results
1330
     * @param string $lang language
1331
     * @return string sparql query
1332
     */
1333
    private function generateFirstCharactersQuery($lang, $classes) {
1334
        $fcl = $this->generateFromClause();
1335
        $classes = (isset($classes) && sizeof($classes) > 0) ? $classes : array('http://www.w3.org/2004/02/skos/core#Concept');
1336
        $values = $this->formatValues('?type', $classes, 'uri');
1337
        $query = <<<EOQ
1338
SELECT DISTINCT (ucase(str(substr(?label, 1, 1))) as ?l) $fcl WHERE {
1339
  ?c skos:prefLabel ?label .
1340
  ?c a ?type
1341
  FILTER(langMatches(lang(?label), '$lang'))
1342
  $values
1343
}
1344
EOQ;
1345
        return $query;
1346
    }
1347
1348
    /**
1349
     * Transforms the first characters query results into an array format.
1350
     * @param EasyRdf\Sparql\Result $result
1351
     * @return array
1352
     */
1353
    private function transformFirstCharactersResults($result) {
1354
        $ret = array();
1355
        foreach ($result as $row) {
1356
            $ret[] = $row->l->getValue();
1357
        }
1358
        return $ret;
1359
    }
1360
1361
    /**
1362
     * Query for the first characters (letter or otherwise) of the labels in the particular language.
1363
     * @param string $lang language
1364
     * @return array array of characters
1365
     */
1366
    public function queryFirstCharacters($lang, $classes = null) {
1367
        $query = $this->generateFirstCharactersQuery($lang, $classes);
1368
        $result = $this->query($query);
1369
        return $this->transformFirstCharactersResults($result);
0 ignored issues
show
Bug introduced by
It seems like $result defined by $this->query($query) on line 1368 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...
1370
    }
1371
1372
    /**
1373
     * @param string $uri
1374
     * @param string $lang
1375
     * @return string sparql query string
1376
     */
1377
    private function generateLabelQuery($uri, $lang) {
1378
        $fcl = $this->generateFromClause();
1379
        $labelcondLabel = ($lang) ? "FILTER( langMatches(lang(?label), '$lang') )" : "";
1380
        $query = <<<EOQ
1381
SELECT ?label $fcl
1382
WHERE {
1383
  <$uri> a ?type .
1384
  OPTIONAL {
1385
    <$uri> skos:prefLabel ?label .
1386
    $labelcondLabel
1387
  }
1388
  OPTIONAL {
1389
    <$uri> rdfs:label ?label .
1390
    $labelcondLabel
1391
  }
1392
  OPTIONAL {
1393
    <$uri> dc:title ?label .
1394
    $labelcondLabel
1395
  }
1396
  OPTIONAL {
1397
    <$uri> dc11:title ?label .
1398
    $labelcondLabel
1399
  }
1400
}
1401
EOQ;
1402
        return $query;
1403
    }
1404
1405
1406
    /**
1407
     * @param string $uri
1408
     * @param string $lang
1409
     * @return string sparql query string
1410
     */
1411
    private function generateAllLabelsQuery($uri, $lang) {
1412
        $fcl = $this->generateFromClause();
1413
        $labelcondLabel = ($lang) ? "FILTER( langMatches(lang(?val), '$lang') )" : "";
1414
        $query = <<<EOQ
1415
SELECT DISTINCT ?prop ?val $fcl
1416
WHERE {
1417
  <$uri> a ?type .
1418
  OPTIONAL {
1419
      <$uri> ?prop ?val .
1420
      $labelcondLabel
1421
  }
1422
  VALUES ?prop { skos:prefLabel skos:altLabel skos:hiddenLabel }
1423
}
1424
EOQ;
1425
        return $query;
1426
    }
1427
1428
    /**
1429
     * Query for a label (skos:prefLabel, rdfs:label, dc:title, dc11:title) of a resource.
1430
     * @param string $uri
1431
     * @param string $lang
1432
     * @return array array of labels (key: lang, val: label), or null if resource doesn't exist
1433
     */
1434 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...
1435
        $query = $this->generateLabelQuery($uri, $lang);
1436
        $result = $this->query($query);
1437
        $ret = array();
1438
        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...
1439
            if (!isset($row->label)) {
1440
                // existing concept but no labels
1441
                return array();
1442
            }
1443
            $ret[$row->label->getLang()] = $row->label;
1444
        }
1445
1446
        if (sizeof($ret) > 0) {
1447
            // existing concept, with label(s)
1448
            return $ret;
1449
        } else {
1450
            // nonexistent concept
1451
            return null;
1452
        }
1453
    }
1454
1455
    /**
1456
     * Query for skos:prefLabels, skos:altLabels and skos:hiddenLabels of a resource.
1457
     * @param string $uri
1458
     * @param string $lang
1459
     * @return array array of prefLabels, altLabels and hiddenLabels - or null if resource doesn't exist
1460
     */
1461
    public function queryAllConceptLabels($uri, $lang) {
1462
        $query = $this->generateAllLabelsQuery($uri, $lang);
1463
        $result = $this->query($query);
1464
1465
        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...
1466
            // nonexistent concept
1467
            return null;
1468
        }
1469
1470
        $ret = array();
1471
        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...
1472
            $labelName = $row->prop->localName();
1473
            if (isset($row->val)) {
1474
                $ret[$labelName][] = $row->val->getValue();
1475
            }
1476
        }
1477
        return $ret;
1478
    }
1479
1480
    /**
1481
     * Generates a SPARQL query to retrieve the super properties of a given property URI.
1482
     * Note this must be executed in the graph where this information is available.
1483
     * @param string $uri
1484
     * @return string sparql query string
1485
     */
1486
    private function generateSubPropertyOfQuery($uri) {
1487
        $fcl = $this->generateFromClause();
1488
        $query = <<<EOQ
1489
SELECT ?superProperty $fcl
1490
WHERE {
1491
  <$uri> rdfs:subPropertyOf ?superProperty
1492
}
1493
EOQ;
1494
        return $query;
1495
    }
1496
1497
    /**
1498
     * Query the super properties of a provided property URI.
1499
     * @param string $uri URI of a propertyes
1500
     * @return array array super properties, or null if none exist
1501
     */
1502 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...
1503
        $query = $this->generateSubPropertyOfQuery($uri);
1504
        $result = $this->query($query);
1505
        $ret = array();
1506
        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...
1507
            if (isset($row->superProperty)) {
1508
                $ret[] = $row->superProperty->getUri();
1509
            }
1510
1511
        }
1512
1513
        if (sizeof($ret) > 0) {
1514
            // return result
1515
            return $ret;
1516
        } else {
1517
            // no result, return null
1518
            return null;
1519
        }
1520
    }
1521
1522
1523
    /**
1524
     * Generates a sparql query for queryNotation.
1525
     * @param string $uri
1526
     * @return string sparql query
1527
     */
1528
    private function generateNotationQuery($uri) {
1529
        $fcl = $this->generateFromClause();
1530
1531
        $query = <<<EOQ
1532
SELECT * $fcl
1533
WHERE {
1534
  <$uri> skos:notation ?notation .
1535
}
1536
EOQ;
1537
        return $query;
1538
    }
1539
1540
    /**
1541
     * Query for the notation of the concept (skos:notation) of a resource.
1542
     * @param string $uri
1543
     * @return string notation or null if it doesn't exist
1544
     */
1545
    public function queryNotation($uri) {
1546
        $query = $this->generateNotationQuery($uri);
1547
        $result = $this->query($query);
1548
        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...
1549
            if (isset($row->notation)) {
1550
                return $row->notation->getValue();
1551
            }
1552
        }
1553
        return null;
1554
    }
1555
1556
    /**
1557
     * Generates a sparql query for queryProperty.
1558
     * @param string $uri
1559
     * @param string $prop the name of the property eg. 'skos:broader'.
1560
     * @param string $lang
1561
     * @param boolean $anylang if you want a label even when it isn't available in the language you requested.
1562
     * @return string sparql query
1563
     */
1564 View Code Duplication
    private function generatePropertyQuery($uri, $prop, $lang, $anylang) {
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...
1565
        $fcl = $this->generateFromClause();
1566
        $anylang = $anylang ? "OPTIONAL { ?object skos:prefLabel ?label }" : "";
1567
1568
        $query = <<<EOQ
1569
SELECT * $fcl
1570
WHERE {
1571
  <$uri> a skos:Concept .
1572
  OPTIONAL {
1573
    <$uri> $prop ?object .
1574
    OPTIONAL {
1575
      ?object skos:prefLabel ?label .
1576
      FILTER (langMatches(lang(?label), "$lang"))
1577
    }
1578
    OPTIONAL {
1579
      ?object skos:prefLabel ?label .
1580
      FILTER (lang(?label) = "")
1581
    }
1582
    $anylang
1583
  }
1584
}
1585
EOQ;
1586
        return $query;
1587
    }
1588
1589
    /**
1590
     * Transforms the sparql query result into an array or null if the concept doesn't exist.
1591
     * @param EasyRdf\Sparql\Result $result
1592
     * @param string $lang
1593
     * @return array array of property values (key: URI, val: label), or null if concept doesn't exist
1594
     */
1595
    private function transformPropertyQueryResults($result, $lang) {
1596
        $ret = array();
1597
        foreach ($result as $row) {
1598
            if (!isset($row->object)) {
1599
                return array();
1600
            }
1601
            // existing concept but no properties
1602
            if (isset($row->label)) {
1603
                if ($row->label->getLang() === $lang || array_key_exists($row->object->getUri(), $ret) === false) {
1604
                    $ret[$row->object->getUri()]['label'] = $row->label->getValue();
1605
                }
1606
1607
            } else {
1608
                $ret[$row->object->getUri()]['label'] = null;
1609
            }
1610
        }
1611
        if (sizeof($ret) > 0) {
1612
            return $ret;
1613
        }
1614
        // existing concept, with properties
1615
        else {
1616
            return null;
1617
        }
1618
        // nonexistent concept
1619
    }
1620
1621
    /**
1622
     * Query a single property of a concept.
1623
     * @param string $uri
1624
     * @param string $prop the name of the property eg. 'skos:broader'.
1625
     * @param string $lang
1626
     * @param boolean $anylang if you want a label even when it isn't available in the language you requested.
1627
     * @return array array of property values (key: URI, val: label), or null if concept doesn't exist
1628
     */
1629
    public function queryProperty($uri, $prop, $lang, $anylang = false) {
1630
        $uri = is_array($uri) ? $uri[0] : $uri;
1631
        $query = $this->generatePropertyQuery($uri, $prop, $lang, $anylang);
1632
        $result = $this->query($query);
1633
        return $this->transformPropertyQueryResults($result, $lang);
0 ignored issues
show
Bug introduced by
It seems like $result defined by $this->query($query) on line 1632 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...
1634
    }
1635
1636
    /**
1637
     * Query a single transitive property of a concept.
1638
     * @param string $uri
1639
     * @param array $props the name of the property eg. 'skos:broader'.
1640
     * @param string $lang
1641
     * @param integer $limit
1642
     * @param boolean $anylang if you want a label even when it isn't available in the language you requested.
1643
     * @return string sparql query
1644
     */
1645
    private function generateTransitivePropertyQuery($uri, $props, $lang, $limit, $anylang) {
1646
        $uri = is_array($uri) ? $uri[0] : $uri;
1647
        $fcl = $this->generateFromClause();
1648
        $propertyClause = implode('|', $props);
1649
        $otherlang = $anylang ? "OPTIONAL { ?object skos:prefLabel ?label }" : "";
1650
        // need to do a SPARQL subquery because LIMIT needs to be applied /after/
1651
        // the direct relationships have been collapsed into one string
1652
        $query = <<<EOQ
1653
SELECT * $fcl
1654
WHERE {
1655
  SELECT ?object ?label (GROUP_CONCAT(STR(?dir);separator=' ') as ?direct)
1656
  WHERE {
1657
    <$uri> a skos:Concept .
1658
    OPTIONAL {
1659
      <$uri> $propertyClause* ?object .
1660
      OPTIONAL {
1661
        ?object $propertyClause ?dir .
1662
      }
1663
    }
1664
    OPTIONAL {
1665
      ?object skos:prefLabel ?label .
1666
      FILTER (langMatches(lang(?label), "$lang"))
1667
    }
1668
    $otherlang
1669
  }
1670
  GROUP BY ?object ?label
1671
}
1672
LIMIT $limit
1673
EOQ;
1674
        return $query;
1675
    }
1676
1677
    /**
1678
     * Transforms the sparql query result object into an array.
1679
     * @param EasyRdf\Sparql\Result $result
1680
     * @param string $lang
1681
     * @param string $fallbacklang language to use if label is not available in the preferred language
1682
     * @return array of property values (key: URI, val: label), or null if concept doesn't exist
1683
     */
1684
    private function transformTransitivePropertyResults($result, $lang, $fallbacklang) {
1685
        $ret = array();
1686
        foreach ($result as $row) {
1687
            if (!isset($row->object)) {
1688
                return array();
1689
            }
1690
            // existing concept but no properties
1691
            if (isset($row->label)) {
1692
                $val = array('label' => $row->label->getValue());
1693
            } else {
1694
                $val = array('label' => null);
1695
            }
1696 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...
1697
                $val['direct'] = explode(' ', $row->direct->getValue());
1698
            }
1699
            // Preventing labels in a non preferred language overriding the preferred language.
1700
            if (isset($row->label) && $row->label->getLang() === $lang || array_key_exists($row->object->getUri(), $ret) === false) {
1701
                if (!isset($row->label) || $row->label->getLang() === $lang) {
1702
                    $ret[$row->object->getUri()] = $val;
1703
                } elseif ($row->label->getLang() === $fallbacklang) {
1704
                    $val['label'] .= ' (' . $row->label->getLang() . ')';
1705
                    $ret[$row->object->getUri()] = $val;
1706
                }
1707
            }
1708
        }
1709
1710
        // second iteration of results to find labels for the ones that didn't have one in the preferred languages
1711
        foreach ($result as $row) {
1712
            if (isset($row->object) && array_key_exists($row->object->getUri(), $ret) === false) {
1713
                $val = array('label' => $row->label->getValue());
1714 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...
1715
                    $val['direct'] = explode(' ', $row->direct->getValue());
1716
                }
1717
                $ret[$row->object->getUri()] = $val;
1718
            }
1719
        }
1720
1721
        if (sizeof($ret) > 0) {
1722
            return $ret;
1723
        }
1724
        // existing concept, with properties
1725
        else {
1726
            return null;
1727
        }
1728
        // nonexistent concept
1729
    }
1730
1731
    /**
1732
     * Query a single transitive property of a concept.
1733
     * @param string $uri
1734
     * @param array $props the property/properties.
1735
     * @param string $lang
1736
     * @param string $fallbacklang language to use if label is not available in the preferred language
1737
     * @param integer $limit
1738
     * @param boolean $anylang if you want a label even when it isn't available in the language you requested.
1739
     * @return array array of property values (key: URI, val: label), or null if concept doesn't exist
1740
     */
1741
    public function queryTransitiveProperty($uri, $props, $lang, $limit, $anylang = false, $fallbacklang = '') {
1742
        $query = $this->generateTransitivePropertyQuery($uri, $props, $lang, $limit, $anylang);
1743
        $result = $this->query($query);
1744
        return $this->transformTransitivePropertyResults($result, $lang, $fallbacklang);
0 ignored issues
show
Bug introduced by
It seems like $result defined by $this->query($query) on line 1743 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...
1745
    }
1746
1747
    /**
1748
     * Generates the query for a concepts skos:narrowers.
1749
     * @param string $uri
1750
     * @param string $lang
1751
     * @param string $fallback
1752
     * @return string sparql query
1753
     */
1754
    private function generateChildQuery($uri, $lang, $fallback, $props) {
1755
        $uri = is_array($uri) ? $uri[0] : $uri;
1756
        $fcl = $this->generateFromClause();
1757
        $propertyClause = implode('|', $props);
1758
        $query = <<<EOQ
1759
SELECT ?child ?label ?child ?grandchildren ?notation $fcl WHERE {
1760
  <$uri> a skos:Concept .
1761
  OPTIONAL {
1762
    ?child $propertyClause <$uri> .
1763
    OPTIONAL {
1764
      ?child skos:prefLabel ?label .
1765
      FILTER (langMatches(lang(?label), "$lang"))
1766
    }
1767
    OPTIONAL {
1768
      ?child skos:prefLabel ?label .
1769
      FILTER (langMatches(lang(?label), "$fallback"))
1770
    }
1771
    OPTIONAL { # other language case
1772
      ?child skos:prefLabel ?label .
1773
    }
1774
    OPTIONAL {
1775
      ?child skos:notation ?notation .
1776
    }
1777
    BIND ( EXISTS { ?a $propertyClause ?child . } AS ?grandchildren )
1778
  }
1779
}
1780
EOQ;
1781
        return $query;
1782
    }
1783
1784
    /**
1785
     * Transforms the sparql result object into an array.
1786
     * @param EasyRdf\Sparql\Result $result
1787
     * @param string $lang
1788
     * @return array array of arrays describing each child concept, or null if concept doesn't exist
1789
     */
1790
    private function transformNarrowerResults($result, $lang) {
1791
        $ret = array();
1792
        foreach ($result as $row) {
1793
            if (!isset($row->child)) {
1794
                return array();
1795
            }
1796
            // existing concept but no children
1797
1798
            $label = null;
1799
            if (isset($row->label)) {
1800
                if ($row->label->getLang() == $lang || strpos($row->label->getLang(), $lang . "-") == 0) {
1801
                    $label = $row->label->getValue();
1802
                } else {
1803
                    $label = $row->label->getValue() . " (" . $row->label->getLang() . ")";
1804
                }
1805
1806
            }
1807
            $childArray = array(
1808
                'uri' => $row->child->getUri(),
1809
                'prefLabel' => $label,
1810
                'hasChildren' => filter_var($row->grandchildren->getValue(), FILTER_VALIDATE_BOOLEAN),
1811
            );
1812
            if (isset($row->notation)) {
1813
                $childArray['notation'] = $row->notation->getValue();
1814
            }
1815
1816
            $ret[] = $childArray;
1817
        }
1818
        if (sizeof($ret) > 0) {
1819
            return $ret;
1820
        }
1821
        // existing concept, with children
1822
        else {
1823
            return null;
1824
        }
1825
        // nonexistent concept
1826
    }
1827
1828
    /**
1829
     * Query the narrower concepts of a concept.
1830
     * @param string $uri
1831
     * @param string $lang
1832
     * @param string $fallback
1833
     * @return array array of arrays describing each child concept, or null if concept doesn't exist
1834
     */
1835
    public function queryChildren($uri, $lang, $fallback, $props) {
1836
        $query = $this->generateChildQuery($uri, $lang, $fallback, $props);
1837
        $result = $this->query($query);
1838
        return $this->transformNarrowerResults($result, $lang);
0 ignored issues
show
Bug introduced by
It seems like $result defined by $this->query($query) on line 1837 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...
1839
    }
1840
1841
    /**
1842
     * Query the top concepts of a vocabulary.
1843
     * @param string $conceptSchemes concept schemes whose top concepts to query for
1844
     * @param string $lang language of labels
1845
     * @param string $fallback language to use if label is not available in the preferred language
1846
     */
1847
    public function queryTopConcepts($conceptSchemes, $lang, $fallback) {
1848
        if (!is_array($conceptSchemes)) {
1849
            $conceptSchemes = array($conceptSchemes);
1850
        }
1851
1852
        $values = $this->formatValues('?topuri', $conceptSchemes, 'uri');
1853
1854
        $fcl = $this->generateFromClause();
1855
        $query = <<<EOQ
1856
SELECT DISTINCT ?top ?topuri ?label ?notation ?children $fcl WHERE {
1857
  ?top skos:topConceptOf ?topuri .
1858
  OPTIONAL {
1859
    ?top skos:prefLabel ?label .
1860
    FILTER (langMatches(lang(?label), "$lang"))
1861
  }
1862
  OPTIONAL {
1863
    ?top skos:prefLabel ?label .
1864
    FILTER (langMatches(lang(?label), "$fallback"))
1865
  }
1866
  OPTIONAL { # fallback - other language case
1867
    ?top skos:prefLabel ?label .
1868
  }
1869
  OPTIONAL { ?top skos:notation ?notation . }
1870
  BIND ( EXISTS { ?top skos:narrower ?a . } AS ?children )
1871
  $values
1872
}
1873
EOQ;
1874
        $result = $this->query($query);
1875
        $ret = array();
1876
        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...
1877
            if (isset($row->top) && isset($row->label)) {
1878
                $label = $row->label->getValue();
1879 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...
1880
                    $label .= ' (' . $row->label->getLang() . ')';
1881
                }
1882
                $top = array('uri' => $row->top->getUri(), 'topConceptOf' => $row->topuri->getUri(), 'label' => $label, 'hasChildren' => filter_var($row->children->getValue(), FILTER_VALIDATE_BOOLEAN));
1883
                if (isset($row->notation)) {
1884
                    $top['notation'] = $row->notation->getValue();
1885
                }
1886
1887
                $ret[] = $top;
1888
            }
1889
        }
1890
1891
        return $ret;
1892
    }
1893
1894
    /**
1895
     * Generates a sparql query for finding the hierarchy for a concept.
1896
	 * A concept may be a top concept in multiple schemes, returned as a single whitespace-separated literal.
1897
     * @param string $uri concept uri.
1898
     * @param string $lang
1899
     * @param string $fallback language to use if label is not available in the preferred language
1900
     * @return string sparql query
1901
     */
1902
    private function generateParentListQuery($uri, $lang, $fallback, $props) {
1903
        $fcl = $this->generateFromClause();
1904
        $propertyClause = implode('|', $props);
1905
        $query = <<<EOQ
1906
SELECT ?broad ?parent ?children ?grandchildren
1907
(SAMPLE(?lab) as ?label) (SAMPLE(?childlab) as ?childlabel) (GROUP_CONCAT(?topcs; separator=" ") as ?tops) 
1908
(SAMPLE(?nota) as ?notation) (SAMPLE(?childnota) as ?childnotation) $fcl
1909
WHERE {
1910
  <$uri> a skos:Concept .
1911
  OPTIONAL {
1912
    <$uri> $propertyClause* ?broad .
1913
    OPTIONAL {
1914
      ?broad skos:prefLabel ?lab .
1915
      FILTER (langMatches(lang(?lab), "$lang"))
1916
    }
1917
    OPTIONAL {
1918
      ?broad skos:prefLabel ?lab .
1919
      FILTER (langMatches(lang(?lab), "$fallback"))
1920
    }
1921
    OPTIONAL { # fallback - other language case
1922
      ?broad skos:prefLabel ?lab .
1923
    }
1924
    OPTIONAL { ?broad skos:notation ?nota . }
1925
    OPTIONAL { ?broad $propertyClause ?parent . }
1926
    OPTIONAL { ?broad skos:narrower ?children .
1927
      OPTIONAL {
1928
        ?children skos:prefLabel ?childlab .
1929
        FILTER (langMatches(lang(?childlab), "$lang"))
1930
      }
1931
      OPTIONAL {
1932
        ?children skos:prefLabel ?childlab .
1933
        FILTER (langMatches(lang(?childlab), "$fallback"))
1934
      }
1935
      OPTIONAL { # fallback - other language case
1936
        ?children skos:prefLabel ?childlab .
1937
      }
1938
      OPTIONAL {
1939
        ?children skos:notation ?childnota .
1940
      }
1941
    }
1942
    BIND ( EXISTS { ?children skos:narrower ?a . } AS ?grandchildren )
1943
    OPTIONAL { ?broad skos:topConceptOf ?topcs . }
1944
  }
1945
}
1946
GROUP BY ?broad ?parent ?member ?children ?grandchildren
1947
EOQ;
1948
        return $query;
1949
    }
1950
1951
    /**
1952
     * Transforms the result into an array.
1953
     * @param EasyRdf\Sparql\Result
1954
     * @param string $lang
1955
     * @return an array for the REST controller to encode.
1956
     */
1957
    private function transformParentListResults($result, $lang)
1958
    {
1959
        $ret = array();
1960
        foreach ($result as $row) {
1961
            if (!isset($row->broad)) {
1962
                // existing concept but no broaders
1963
                return array();
1964
            }
1965
            $uri = $row->broad->getUri();
1966
            if (!isset($ret[$uri])) {
1967
                $ret[$uri] = array('uri' => $uri);
1968
            }
1969
            if (isset($row->exact)) {
1970
                $ret[$uri]['exact'] = $row->exact->getUri();
1971
            }
1972
            if (isset($row->tops)) {
1973
               $topConceptsList=explode(" ", $row->tops->getValue());
1974
               // sort to garantee an alphabetical ordering of the URI
1975
               sort($topConceptsList);
1976
               $ret[$uri]['tops'] = $topConceptsList;
1977
            }
1978
            if (isset($row->children)) {
1979
                if (!isset($ret[$uri]['narrower'])) {
1980
                    $ret[$uri]['narrower'] = array();
1981
                }
1982
1983
                $label = null;
1984
                if (isset($row->childlabel)) {
1985
                    $label = $row->childlabel->getValue();
1986 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...
1987
                        $label .= " (" . $row->childlabel->getLang() . ")";
1988
                    }
1989
1990
                }
1991
1992
                $childArr = array(
1993
                    'uri' => $row->children->getUri(),
1994
                    'label' => $label,
1995
                    'hasChildren' => filter_var($row->grandchildren->getValue(), FILTER_VALIDATE_BOOLEAN),
1996
                );
1997
                if (isset($row->childnotation)) {
1998
                    $childArr['notation'] = $row->childnotation->getValue();
1999
                }
2000
2001
                if (!in_array($childArr, $ret[$uri]['narrower'])) {
2002
                    $ret[$uri]['narrower'][] = $childArr;
2003
                }
2004
2005
            }
2006
            if (isset($row->label)) {
2007
                $preflabel = $row->label->getValue();
2008 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...
2009
                    $preflabel .= ' (' . $row->label->getLang() . ')';
2010
                }
2011
2012
                $ret[$uri]['prefLabel'] = $preflabel;
2013
            }
2014
            if (isset($row->notation)) {
2015
                $ret[$uri]['notation'] = $row->notation->getValue();
2016
            }
2017
2018
            if (isset($row->parent) && (isset($ret[$uri]['broader']) && !in_array($row->parent->getUri(), $ret[$uri]['broader']))) {
2019
                $ret[$uri]['broader'][] = $row->parent->getUri();
2020
            } elseif (isset($row->parent) && !isset($ret[$uri]['broader'])) {
2021
                $ret[$uri]['broader'][] = $row->parent->getUri();
2022
            }
2023
        }
2024
        if (sizeof($ret) > 0) {
2025
            // existing concept, with children
2026
            return $ret;
2027
        }
2028
        else {
2029
            // nonexistent concept
2030
            return null;
2031
        }
2032
    }
2033
2034
    /**
2035
     * Query for finding the hierarchy for a concept.
2036
     * @param string $uri concept uri.
2037
     * @param string $lang
2038
     * @param string $fallback language to use if label is not available in the preferred language
2039
     * @param array $props the hierarchy property/properties to use
2040
     * @return an array for the REST controller to encode.
2041
     */
2042
    public function queryParentList($uri, $lang, $fallback, $props) {
2043
        $query = $this->generateParentListQuery($uri, $lang, $fallback, $props);
2044
        $result = $this->query($query);
2045
        return $this->transformParentListResults($result, $lang);
2046
    }
2047
2048
    /**
2049
     * return a list of concept group instances, sorted by label
2050
     * @param string $groupClass URI of concept group class
2051
     * @param string $lang language of labels to return
2052
     * @return string sparql query
2053
     */
2054
    private function generateConceptGroupsQuery($groupClass, $lang) {
2055
        $fcl = $this->generateFromClause();
2056
        $query = <<<EOQ
2057
SELECT ?group (GROUP_CONCAT(DISTINCT STR(?child);separator=' ') as ?children) ?label ?members ?notation $fcl
2058
WHERE {
2059
  ?group a <$groupClass> .
2060
  OPTIONAL { ?group skos:member|isothes:subGroup ?child .
2061
             ?child a <$groupClass> }
2062
  BIND(EXISTS{?group skos:member ?submembers} as ?members)
2063
  OPTIONAL { ?group skos:prefLabel ?label }
2064
  OPTIONAL { ?group rdfs:label ?label }
2065
  FILTER (langMatches(lang(?label), '$lang'))
2066
  OPTIONAL { ?group skos:notation ?notation }
2067
}
2068
GROUP BY ?group ?label ?members ?notation
2069
ORDER BY lcase(?label)
2070
EOQ;
2071
        return $query;
2072
    }
2073
2074
    /**
2075
     * Transforms the sparql query result into an array.
2076
     * @param EasyRdf\Sparql\Result $result
2077
     * @return array
2078
     */
2079
    private function transformConceptGroupsResults($result) {
2080
        $ret = array();
2081
        foreach ($result as $row) {
2082
            if (!isset($row->group)) {
2083
                # no groups found, see issue #357
2084
                continue;
2085
            }
2086
            $group = array('uri' => $row->group->getURI());
2087
            if (isset($row->label)) {
2088
                $group['prefLabel'] = $row->label->getValue();
2089
            }
2090
2091
            if (isset($row->children)) {
2092
                $group['childGroups'] = explode(' ', $row->children->getValue());
2093
            }
2094
2095
            if (isset($row->members)) {
2096
                $group['hasMembers'] = $row->members->getValue();
2097
            }
2098
2099
            if (isset($row->notation)) {
2100
                $group['notation'] = $row->notation->getValue();
2101
            }
2102
2103
            $ret[] = $group;
2104
        }
2105
        return $ret;
2106
    }
2107
2108
    /**
2109
     * return a list of concept group instances, sorted by label
2110
     * @param string $groupClass URI of concept group class
2111
     * @param string $lang language of labels to return
2112
     * @return array Result array with group URI as key and group label as value
2113
     */
2114
    public function listConceptGroups($groupClass, $lang) {
2115
        $query = $this->generateConceptGroupsQuery($groupClass, $lang);
2116
        $result = $this->query($query);
2117
        return $this->transformConceptGroupsResults($result);
0 ignored issues
show
Bug introduced by
It seems like $result defined by $this->query($query) on line 2116 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...
2118
    }
2119
2120
    /**
2121
     * Generates the sparql query for listConceptGroupContents
2122
     * @param string $groupClass URI of concept group class
2123
     * @param string $group URI of the concept group instance
2124
     * @param string $lang language of labels to return
2125
     * @param boolean $showDeprecated whether to include deprecated in the result
2126
     * @return string sparql query
2127
     */
2128
    private function generateConceptGroupContentsQuery($groupClass, $group, $lang, $showDeprecated = false) {
2129
        $fcl = $this->generateFromClause();
2130
        $filterDeprecated="";
2131
        if(!$showDeprecated){
2132
            $filterDeprecated="  FILTER NOT EXISTS { ?conc owl:deprecated true }";
2133
        }
2134
        $query = <<<EOQ
2135
SELECT ?conc ?super ?label ?members ?type ?notation $fcl
2136
WHERE {
2137
 <$group> a <$groupClass> .
2138
 { <$group> skos:member ?conc . } UNION { ?conc isothes:superGroup <$group> }
2139
$filterDeprecated
2140
 ?conc a ?type .
2141
 OPTIONAL { ?conc skos:prefLabel ?label .
2142
  FILTER (langMatches(lang(?label), '$lang'))
2143
 }
2144
 OPTIONAL { ?conc skos:prefLabel ?label . }
2145
 OPTIONAL { ?conc skos:notation ?notation }
2146
 BIND(EXISTS{?submembers isothes:superGroup ?conc} as ?super)
2147
 BIND(EXISTS{?conc skos:member ?submembers} as ?members)
2148
} ORDER BY lcase(?label)
2149
EOQ;
2150
        return $query;
2151
    }
2152
2153
    /**
2154
     * Transforms the sparql query result into an array.
2155
     * @param EasyRdf\Sparql\Result $result
2156
     * @param string $lang language of labels to return
2157
     * @return array
2158
     */
2159
    private function transformConceptGroupContentsResults($result, $lang) {
2160
        $ret = array();
2161
        $values = array();
2162
        foreach ($result as $row) {
2163
            if (!array_key_exists($row->conc->getURI(), $values)) {
2164
                $values[$row->conc->getURI()] = array(
2165
                    'uri' => $row->conc->getURI(),
2166
                    'isSuper' => $row->super->getValue(),
2167
                    'hasMembers' => $row->members->getValue(),
2168
                    'type' => array($row->type->shorten()),
2169
                );
2170
                if (isset($row->label)) {
2171
                    if ($row->label->getLang() == $lang || strpos($row->label->getLang(), $lang . "-") == 0) {
2172
                        $values[$row->conc->getURI()]['prefLabel'] = $row->label->getValue();
2173
                    } else {
2174
                        $values[$row->conc->getURI()]['prefLabel'] = $row->label->getValue() . " (" . $row->label->getLang() . ")";
2175
                    }
2176
2177
                }
2178
                if (isset($row->notation)) {
2179
                    $values[$row->conc->getURI()]['notation'] = $row->notation->getValue();
2180
                }
2181
2182
            } else {
2183
                $values[$row->conc->getURI()]['type'][] = $row->type->shorten();
2184
            }
2185
        }
2186
2187
        foreach ($values as $val) {
2188
            $ret[] = $val;
2189
        }
2190
2191
        return $ret;
2192
    }
2193
2194
    /**
2195
     * return a list of concepts in a concept group
2196
     * @param string $groupClass URI of concept group class
2197
     * @param string $group URI of the concept group instance
2198
     * @param string $lang language of labels to return
2199
     * @param boolean $showDeprecated whether to include deprecated concepts in search results
2200
     * @return array Result array with concept URI as key and concept label as value
2201
     */
2202
    public function listConceptGroupContents($groupClass, $group, $lang,$showDeprecated = false) {
2203
        $query = $this->generateConceptGroupContentsQuery($groupClass, $group, $lang,$showDeprecated);
2204
        $result = $this->query($query);
2205
        return $this->transformConceptGroupContentsResults($result, $lang);
0 ignored issues
show
Bug introduced by
It seems like $result defined by $this->query($query) on line 2204 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...
2206
    }
2207
2208
    /**
2209
     * Generates the sparql query for queryChangeList.
2210
     * @param string $lang language of labels to return.
2211
     * @param int $offset offset of results to retrieve; 0 for beginning of list
2212
     * @return string sparql query
2213
     */
2214 View Code Duplication
    private function generateChangeListQuery($lang, $offset, $prop) {
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...
2215
        $fcl = $this->generateFromClause();
2216
        $offset = ($offset) ? 'OFFSET ' . $offset : '';
2217
2218
        $query = <<<EOQ
2219
SELECT DISTINCT ?concept ?date ?label $fcl
2220
WHERE {
2221
  ?concept a skos:Concept .
2222
  ?concept $prop ?date .
2223
  ?concept skos:prefLabel ?label .
2224
  FILTER (langMatches(lang(?label), '$lang'))
2225
}
2226
ORDER BY DESC(YEAR(?date)) DESC(MONTH(?date)) LCASE(?label)
2227
LIMIT 200 $offset
2228
EOQ;
2229
        return $query;
2230
    }
2231
2232
    /**
2233
     * Transforms the sparql query result into an array.
2234
     * @param EasyRdf\Sparql\Result $result
2235
     * @return array
2236
     */
2237 View Code Duplication
    private function transformChangeListResults($result) {
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...
2238
        $ret = array();
2239
        foreach ($result as $row) {
2240
            $concept = array('uri' => $row->concept->getURI());
2241
            if (isset($row->label)) {
2242
                $concept['prefLabel'] = $row->label->getValue();
2243
            }
2244
2245
            if (isset($row->date)) {
2246
                $concept['date'] = $row->date->getValue();
2247
            }
2248
2249
            $ret[] = $concept;
2250
        }
2251
        return $ret;
2252
    }
2253
2254
    /**
2255
     * return a list of recently changed or entirely new concepts
2256
     * @param string $lang language of labels to return
2257
     * @param int $offset offset of results to retrieve; 0 for beginning of list
2258
     * @return array Result array
2259
     */
2260
    public function queryChangeList($lang, $offset, $prop) {
2261
        $query = $this->generateChangeListQuery($lang, $offset, $prop);
2262
        $result = $this->query($query);
2263
        return $this->transformChangeListResults($result);
0 ignored issues
show
Bug introduced by
It seems like $result defined by $this->query($query) on line 2262 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...
2264
    }
2265
}
2266