Model::createDataObjects()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 2
dl 0
loc 8
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * Model provides access to the data.
5
 * @property EasyRdf\Graph $graph
6
 * @property GlobalConfig $globalConfig
7
 */
8
class Model
9
{
10
    /** cache for Vocabulary objects */
11
    private $allVocabularies = null;
12
    /** cache for Vocabulary objects */
13
    private $vocabsByGraph = null;
14
    /** cache for Vocabulary objects */
15
    private $vocabsByUriSpace = null;
16
    /** how long to store retrieved URI information in APC cache */
17
    const URI_FETCH_TTL = 86400; // 1 day
18
    private $globalConfig;
19
    private $logger;
20
    private $resolver;
21
22
    /**
23
     * Initializes the Model object
24
     */
25
    public function __construct($config)
26
    {
27
        $this->globalConfig = $config;
28
        $this->initializeLogging();
29
        $this->resolver = new Resolver($this);
30
    }
31
32
    /**
33
     * Returns the GlobalConfig object given to the Model as a constructor parameter.
34
     * @return GlobalConfig
35
     */
36
    public function getConfig() {
37
      return $this->globalConfig;
38
    }
39
40
    /**
41
     * Configures the logging facility
42
     */
43
    private function initializeLogging() {
44
        $this->logger = new \Monolog\Logger('general');
45
        $formatter = new \Monolog\Formatter\LineFormatter("[%datetime%] %level_name% %message%\n");
46
        $formatter->allowInlineLineBreaks(true);
47
        if ($this->getConfig()->getLoggingBrowserConsole()) {
48
            $browserHandler = new \Monolog\Handler\BrowserConsoleHandler(\Monolog\Logger::INFO);
49
            $browserHandler->setFormatter($formatter);
50
            $this->logger->pushHandler($browserHandler);
51
        }
52
        if ($this->getConfig()->getLoggingFilename() !== null) {
0 ignored issues
show
introduced by
The condition $this->getConfig()->getLoggingFilename() !== null is always true.
Loading history...
53
            $streamHandler = new \Monolog\Handler\StreamHandler($this->getConfig()->getLoggingFilename(), \Monolog\Logger::INFO);
54
            $streamHandler->setFormatter($formatter);
55
            $this->logger->pushHandler($streamHandler);
56
        }
57
        if (!$this->logger->getHandlers()) {
58
            // add a NullHandler to suppress the default Monolog logging to stderr
59
            $nullHandler = new \Monolog\Handler\NullHandler();
60
            $this->logger->pushHandler($nullHandler);
61
        }
62
    }
63
64
    /**
65
     * Return the logging facility
66
     * @return object logger
67
     */
68
    public function getLogger() {
69
        return $this->logger;
70
    }
71
72
    /**
73
     * Return the version of this Skosmos installation, or "unknown" if
74
     * it cannot be determined. The version information is based on Git tags.
75
     * @return string version
76
     */
77
    public function getVersion() : string
78
    {
79
        $ver = \Composer\InstalledVersions::getRootPackage()['pretty_version'];
80
        if ($ver === null) {
81
            return "unknown";
82
        }
83
84
        return $ver;
85
    }
86
87
    /**
88
     * Return all the vocabularies available.
89
     * @param boolean $categories whether you want everything included in a subarray of
90
     * a category.
91
     * @param boolean $shortname set to true if you want the vocabularies sorted by
92
     * their shortnames instead of their titles.
93
     */
94
    public function getVocabularyList($categories = true, $shortname = false)
95
    {
96
        $cats = $this->getVocabularyCategories();
97
        $ret = array();
98
        foreach ($cats as $cat) {
99
            $catlabel = $cat->getTitle();
100
101
            // find all the vocabs in this category
102
            $vocs = array();
103
            foreach ($cat->getVocabularies() as $voc) {
104
                $vocs[$shortname ? $voc->getConfig()->getShortname() : $voc->getConfig()->getTitle()] = $voc;
105
            }
106
            uksort($vocs, 'strcoll');
107
108
            if (sizeof($vocs) > 0 && $categories) {
109
                $ret[$catlabel] = $vocs;
110
            } elseif (sizeof($vocs) > 0) {
111
                $ret = array_merge($ret, $vocs);
112
            }
113
114
        }
115
116
        if (!$categories) {
117
            uksort($ret, 'strcoll');
118
        }
119
120
        return $ret;
121
    }
122
123
    /**
124
     * Return all types (RDFS/OWL classes) present in the specified vocabulary or all vocabularies.
125
     * @return array Array with URIs (string) as key and array of (label, superclassURI) as value
126
     */
127
    public function getTypes($vocid = null, $lang = null)
128
    {
129
        $sparql = (isset($vocid)) ? $this->getVocabulary($vocid)->getSparql() : $this->getDefaultSparql();
130
        $result = $sparql->queryTypes($lang);
131
132
        foreach ($result as $uri => $values) {
133
            if (empty($values)) {
134
                $shorteneduri = EasyRdf\RdfNamespace::shorten($uri);
135
                if ($shorteneduri !== null) {
136
                    $trans = gettext($shorteneduri);
137
                    if ($trans) {
138
                        $result[$uri] = array('label' => $trans);
139
                    }
140
                }
141
            }
142
        }
143
144
        return $result;
145
    }
146
147
    /**
148
     * Return the languages present in the configured vocabularies.
149
     * @return array Array with lang codes (string)
150
     */
151
    public function getLanguages($lang)
152
    {
153
        $vocabs = $this->getVocabularyList(false);
154
        $ret = array();
155
        foreach ($vocabs as $vocab) {
156
            foreach ($vocab->getConfig()->getLanguages() as $langcode) {
157
                $langlit = Punic\Language::getName($langcode, $lang);
158
                $ret[$langlit] = $langcode;
159
            }
160
        }
161
        ksort($ret);
162
        return array_unique($ret);
163
    }
164
165
    /**
166
     * returns a concept's RDF data in downloadable format
167
     * @param string $vocid vocabulary id, or null for global data (from all vocabularies)
168
     * @param string $uri concept URI
169
     * @param string $format the format in which you want to get the result, currently this function supports
170
     * text/turtle, application/rdf+xml and application/json
171
     * @return string RDF data in the requested serialization
172
     */
173
    public function getRDF($vocid, $uri, $format)
174
    {
175
176
        if ($format == 'text/turtle') {
177
            $retform = 'turtle';
178
            $serialiser = new EasyRdf\Serialiser\Turtle();
179
        } elseif ($format == 'application/ld+json' || $format == 'application/json') {
180
            $retform = 'jsonld'; // serve JSON-LD for both JSON-LD and plain JSON requests
181
            $serialiser = new EasyRdf\Serialiser\JsonLd();
182
        } else {
183
            $retform = 'rdfxml';
184
            $serialiser = new EasyRdf\Serialiser\RdfXml();
185
        }
186
187
        if ($vocid !== null) {
0 ignored issues
show
introduced by
The condition $vocid !== null is always true.
Loading history...
188
            $vocab = $this->getVocabulary($vocid);
189
            $sparql = $vocab->getSparql();
190
            $arrayClass = $vocab->getConfig()->getArrayClassURI();
191
            $vocabs = array($vocab);
192
        } else {
193
            $sparql = $this->getDefaultSparql();
194
            $arrayClass = null;
195
            $vocabs = null;
196
        }
197
        $result = $sparql->queryConceptInfoGraph($uri, $arrayClass, $vocabs);
198
199
        if (!$result->isEmpty()) {
200
            return $serialiser->serialise($result, $retform);
201
        }
202
        return "";
203
    }
204
205
    /**
206
     * Makes a SPARQL-query to the endpoint that retrieves concept
207
     * references as it's search results.
208
     * @param ConceptSearchParameters $params an object that contains all the parameters needed for the search
209
     * @return array search results
210
     */
211
    public function searchConcepts($params)
212
    {
213
        // don't even try to search for empty prefix if no other search criteria (group or parent concept) has been set
214
        if (($params->getSearchTerm() === "" || !preg_match('/[^*]/', $params->getSearchTerm())) && !$params->getGroupLimit() && !$params->getParentLimit()) {
215
            return array();
216
        }
217
218
        $vocabs = $params->getVocabs();
219
        $showDeprecated=false;
220
        if (sizeof($vocabs) === 1) { // search within vocabulary
221
            $voc = $vocabs[0];
222
            $sparql = $voc->getSparql();
223
            $showDeprecated=$voc->getConfig()->getShowDeprecated();
224
        } else { // multi-vocabulary or global search
225
            $voc = null;
226
            $sparql = $this->getDefaultSparql();
227
            // @TODO : in a global search showDeprecated will always be false and cannot be set globally
228
        }
229
230
        $results = $sparql->queryConcepts($vocabs, $params->getAdditionalFields(), $params->getUnique(), $params,$showDeprecated);
231
        if ($params->getRest() && $results && $params->getSearchLimit() !== 0) {
232
          $results = array_slice($results, $params->getOffset(), $params->getSearchLimit());
233
        }
234
        $ret = array();
235
236
        foreach ($results as $hit) {
237
            if (sizeof($vocabs) == 1) {
238
                $hitvoc = $voc;
239
                $hit['vocab'] = $vocabs[0]->getId();
240
            } else {
241
                try {
242
                    $hitvoc = $this->getVocabularyByGraph($hit['graph']);
243
                    $hit['vocab'] = $hitvoc->getId();
244
                } catch (ValueError $e) {
245
                    trigger_error($e->getMessage(), E_USER_WARNING);
246
                    $hitvoc = null;
247
                    $hit['vocab'] = "???";
248
                }
249
            }
250
            unset($hit['graph']);
251
252
            $hit['voc'] = $hitvoc;
253
254
            if ($hitvoc === null || !$hitvoc->containsURI($hit['uri'])) {
255
                // if uri is a external vocab uri that is included in the current vocab
256
                $realvoc = $this->guessVocabularyFromURI($hit['uri'], $voc !== null ? $voc->getId() : null);
257
                if ($realvoc !== $hitvoc) {
258
                    unset($hit['localname']);
259
                    $hit['exvocab'] = ($realvoc !== null) ? $realvoc->getId() : "???";
260
                }
261
            }
262
263
            $ret[] = $hit;
264
        }
265
266
        return $ret;
267
    }
268
269
    /**
270
     * Function for performing a search for concepts and their data fields.
271
     * @param ConceptSearchParameters $params an object that contains all the parameters needed for the search
272
     * @return array array with keys 'count' and 'results'
273
     */
274
    public function searchConceptsAndInfo($params)
275
    {
276
        $params->setUnique(true);
277
        $allhits = $this->searchConcepts($params);
278
        $count = sizeof($allhits);
279
        $hits = array_slice($allhits, intval($params->getOffset()), $params->getSearchLimit());
280
281
        $ret = array();
282
        $uris = array();
283
        $vocabs = array();
284
        $uniqueVocabs = array();
285
        foreach ($hits as $hit) {
286
            $uniqueVocabs[$hit['voc']->getId()] = $hit['voc']->getId();
287
            $vocabs[] = $hit['voc'];
288
            $uris[] = $hit['uri'];
289
        }
290
        if (sizeof($uniqueVocabs) == 1) {
291
            $voc = $vocabs[0];
292
            $sparql = $voc->getSparql();
293
            $arrayClass = $voc->getConfig()->getArrayClassURI();
294
        } else {
295
            $arrayClass = null;
296
            $sparql = $this->getDefaultSparql();
297
        }
298
        if (sizeof($uris) > 0) {
299
            $ret = $sparql->queryConceptInfo($uris, $arrayClass, $vocabs, $params->getSearchLang());
300
        }
301
302
        // For marking that the concept has been found through an alternative label, hidden
303
        // label or a label in another language
304
        foreach ($hits as $idx => $hit) {
305
            if (isset($hit['altLabel']) && isset($ret[$idx])) {
306
                $ret[$idx]->setFoundBy($hit['altLabel'], 'alt');
307
            }
308
309
            if (isset($hit['hiddenLabel']) && isset($ret[$idx])) {
310
                $ret[$idx]->setFoundBy($hit['hiddenLabel'], 'hidden');
311
            }
312
313
            if (isset($hit['matchedPrefLabel'])) {
314
                $ret[$idx]->setFoundBy($hit['matchedPrefLabel'], 'lang');
315
            }
316
317
            if ($ret[$idx] && isset($hit['lang'])) {
318
                $ret[$idx]->setContentLang($hit['lang']);
319
            }
320
        }
321
322
        return array('count' => $count, 'results' => $ret);
323
    }
324
325
    /**
326
     * Creates dataobjects from an input array.
327
     * @param string $class the type of class eg. 'Vocabulary'.
328
     * @param array $resarr contains the EasyRdf\Resources.
329
     */
330
    private function createDataObjects($class, $resarr)
331
    {
332
        $ret = array();
333
        foreach ($resarr as $res) {
334
            $ret[] = new $class($this, $res);
335
        }
336
337
        return $ret;
338
    }
339
340
    /**
341
     * Returns the cached vocabularies.
342
     * @return array of Vocabulary dataobjects
343
     */
344
    public function getVocabularies()
345
    {
346
        if ($this->allVocabularies === null) { // initialize cache
347
            $vocs = $this->globalConfig->getGraph()->allOfType('skosmos:Vocabulary');
348
            $this->allVocabularies = $this->createDataObjects("Vocabulary", $vocs);
349
            foreach ($this->allVocabularies as $voc) {
350
                // register vocabulary ids as RDF namespace prefixes
351
                $prefix = preg_replace('/\W+/', '', $voc->getId()); // strip non-word characters
352
                try {
353
                    if ($prefix != '' && EasyRdf\RdfNamespace::get($prefix) === null) // if not already defined
354
                    {
355
                        EasyRdf\RdfNamespace::set($prefix, $voc->getUriSpace());
356
                    }
357
358
                } catch (Exception $e) {
359
                    // not valid as namespace identifier, ignore
360
                }
361
            }
362
        }
363
364
        return $this->allVocabularies;
365
    }
366
367
    /**
368
     * Returns the cached vocabularies from a category.
369
     * @param EasyRdf\Resource $cat the category in question
370
     * @return array of vocabulary dataobjects
371
     */
372
    public function getVocabulariesInCategory($cat)
373
    {
374
        $vocs = $this->globalConfig->getGraph()->resourcesMatching('dc:subject', $cat);
375
        return $this->createDataObjects("Vocabulary", $vocs);
376
    }
377
378
    /**
379
     * Creates dataobjects of all the different vocabulary categories (Health etc.).
380
     * @return array of Dataobjects of the type VocabularyCategory.
381
     */
382
    public function getVocabularyCategories()
383
    {
384
        $cats = $this->globalConfig->getGraph()->allOfType('skos:Concept');
385
        if(empty($cats)) {
386
            return array(new VocabularyCategory($this, null));
387
        }
388
389
        return $this->createDataObjects("VocabularyCategory", $cats);
390
    }
391
392
    /**
393
     * Returns the label defined in config.ttl with the appropriate language.
394
     * @param string $lang language code of returned labels, eg. 'fi'
395
     * @return string the label for vocabulary categories.
396
     */
397
    public function getClassificationLabel($lang)
398
    {
399
        $cats = $this->globalConfig->getGraph()->allOfType('skos:ConceptScheme');
400
        return $cats ? $cats[0]->label($lang) : null;
0 ignored issues
show
Bug introduced by
The method label() does not exist on EasyRdf\Literal. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

400
        return $cats ? $cats[0]->/** @scrutinizer ignore-call */ label($lang) : null;

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Bug introduced by
The method label() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

400
        return $cats ? $cats[0]->/** @scrutinizer ignore-call */ label($lang) : null;

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
401
    }
402
403
    /**
404
     * Returns a single cached vocabulary.
405
     * @param string $vocid the vocabulary id eg. 'mesh'.
406
     * @return Vocabulary dataobject
407
     */
408
    public function getVocabulary($vocid): Vocabulary
409
    {
410
        $vocs = $this->getVocabularies();
411
        foreach ($vocs as $voc) {
412
            if ($voc->getId() == $vocid) {
413
                return $voc;
414
            }
415
        }
416
        throw new ValueError("Vocabulary id '$vocid' not found in configuration.");
417
    }
418
419
    /**
420
     * Return the vocabulary that is stored in the given graph on the given endpoint.
421
     *
422
     * @param $graph string graph URI
423
     * @param $endpoint string endpoint URL (default SPARQL endpoint if omitted)
424
     * @return Vocabulary vocabulary of this URI, or null if not found
425
     */
426
    public function getVocabularyByGraph($graph, $endpoint = null)
427
    {
428
        if ($endpoint === null) {
429
            $endpoint = $this->getConfig()->getDefaultEndpoint();
430
        }
431
        if ($this->vocabsByGraph === null) { // initialize cache
432
            $this->vocabsByGraph = array();
433
            foreach ($this->getVocabularies() as $voc) {
434
                $key = json_encode(array($voc->getGraph(), $voc->getEndpoint()));
435
                $this->vocabsByGraph[$key] = $voc;
436
            }
437
        }
438
439
        $key = json_encode(array($graph, $endpoint));
440
        if (array_key_exists($key, $this->vocabsByGraph)) {
441
            return $this->vocabsByGraph[$key];
442
        } else {
443
            throw new ValueError("no vocabulary found for graph $graph and endpoint $endpoint");
444
        }
445
446
    }
447
448
    /**
449
     * When multiple vocabularies share same URI namespace, return the
450
     * vocabulary in which the URI is actually defined (has a label).
451
     *
452
     * @param Vocabulary[] $vocabs vocabularies to search
453
     * @param string $uri URI to look for
454
     * @param $preferredVocabId string ID of the preferred vocabulary to return if more than one is found
455
     * @return Vocabulary the vocabulary with the URI
456
     */
457
458
    private function disambiguateVocabulary($vocabs, $uri, $preferredVocabId = null)
459
    {
460
        // if there is only one candidate vocabulary, return it
461
        if (sizeof($vocabs) == 1) {
462
            return $vocabs[0];
463
        }
464
465
        // if there are multiple vocabularies and one is the preferred vocabulary, return it
466
        if($preferredVocabId != null) {
467
            foreach ($vocabs as $vocab) {
468
                if($vocab->getId() == $preferredVocabId) {
469
                    try {
470
                        // double check that a label exists in the preferred vocabulary
471
                        if ($vocab->getConceptLabel($uri, null) !== null) {
472
                            return $vocab;
473
                        } else {
474
                            // not found in preferred vocabulary, fall back to next method
475
                            break;
476
                        }
477
                    } catch (EasyRdf\Http\Exception | EasyRdf\Exception | Throwable $e) {
478
                        if ($this->getConfig()->getLogCaughtExceptions()) {
479
                            error_log('Caught exception: ' . $e->getMessage());
480
                        }
481
                        break;
482
                    }
483
                }
484
            }
485
        }
486
487
        // no preferred vocabulary, or it was not found, search in which vocabulary the concept has a label
488
        foreach ($vocabs as $vocab) {
489
            try {
490
                if ($vocab->getConceptLabel($uri, null) !== null) {
491
                    return $vocab;
492
                }
493
            } catch (EasyRdf\Http\Exception | EasyRdf\Exception | Throwable $e) {
494
                if ($this->getConfig()->getLogCaughtExceptions()) {
495
                    error_log('Caught exception: ' . $e->getMessage());
496
                }
497
                break;
498
            }
499
        }
500
501
        // if the URI couldn't be found, fall back to the first vocabulary
502
        return $vocabs[0];
503
    }
504
505
    /**
506
     * Guess which vocabulary a URI originates from, based on the declared
507
     * vocabulary URI spaces.
508
     *
509
     * @param $uri string URI to search
510
     * @param $preferredVocabId string ID of the preferred vocabulary to return if more than one is found
511
     * @return Vocabulary vocabulary of this URI, or null if not found
512
     */
513
    public function guessVocabularyFromURI($uri, $preferredVocabId = null)
514
    {
515
        if ($this->vocabsByUriSpace === null) { // initialize cache
516
            $this->vocabsByUriSpace = array();
517
            foreach ($this->getVocabularies() as $voc) {
518
                $uriSpace = $voc->getUriSpace();
519
                if ($uriSpace) {
520
                    $this->vocabsByUriSpace[$uriSpace][] = $voc;
521
                }
522
            }
523
        }
524
525
        // try to guess the URI space and look it up in the cache
526
        $res = new EasyRdf\Resource($uri);
527
        $namespace = substr(strval($uri), 0, -strlen(strval($res->localName())));
528
        if ($namespace && array_key_exists($namespace, $this->vocabsByUriSpace)) {
529
            $vocabs = $this->vocabsByUriSpace[$namespace];
530
            return $this->disambiguateVocabulary($vocabs, $uri, $preferredVocabId);
531
        }
532
533
        // didn't work, try to match with each URI space separately
534
        foreach ($this->vocabsByUriSpace as $urispace => $vocabs) {
535
            if (strpos($uri, $urispace) === 0) {
536
                return $this->disambiguateVocabulary($vocabs, $uri, $preferredVocabId);
537
            }
538
        }
539
540
        // not found
541
        return null;
542
    }
543
544
    /**
545
     * Get the label for a resource, preferring 1. the given language 2. configured languages 3. any language.
546
     * @param EasyRdf\Resource $res resource whose label to return
547
     * @param string $lang preferred language
548
     * @return EasyRdf\Literal label as an EasyRdf\Literal object, or null if not found
549
     */
550
    public function getResourceLabel($res, $lang)
551
    {
552
        $langs = array_merge(array($lang), array_keys($this->getConfig()->getLanguages()));
553
        foreach ($langs as $l) {
554
            $label = $res->label($l);
555
            if ($label !== null) {
556
                return $label;
557
            }
558
559
        }
560
        return $res->label(); // desperate check for label in any language; will return null if even this fails
561
    }
562
563
    public function getResourceFromUri($uri)
564
    {
565
        // using apc cache for the resource if available
566
        if ($this->globalConfig->getCache()->isAvailable()) {
567
            // @codeCoverageIgnoreStart
568
            $key = 'fetch: ' . $uri;
569
            $resource = $this->globalConfig->getCache()->fetch($key);
570
            if ($resource === null || $resource === false) { // was not found in cache, or previous request failed
571
                $resource = $this->resolver->resolve($uri, $this->getConfig()->getHttpTimeout());
572
                $this->globalConfig->getCache()->store($key, $resource, self::URI_FETCH_TTL);
573
            }
574
            // @codeCoverageIgnoreEnd
575
        } else { // APC not available, parse on every request
576
            $resource = $this->resolver->resolve($uri, $this->getConfig()->getHttpTimeout());
577
        }
578
        return $resource;
579
    }
580
581
    /**
582
     * Returns a SPARQL endpoint object.
583
     * @param string $dialect eg. 'JenaText'.
584
     * @param string $endpoint url address of endpoint
585
     * @param string|null $graph uri for the target graph.
586
     */
587
    public function getSparqlImplementation($dialect, $endpoint, $graph)
588
    {
589
        $classname = $dialect . "Sparql";
590
591
        return new $classname($endpoint, $graph, $this);
592
    }
593
594
    /**
595
     * Returns a SPARQL endpoint object using the default implementation set in the config.ttl.
596
     */
597
    public function getDefaultSparql()
598
    {
599
        return $this->getSparqlImplementation($this->getConfig()->getDefaultSparqlDialect(), $this->getConfig()->getDefaultEndpoint(), '?graph');
600
    }
601
602
}
603