Completed
Push — master ( cd9b94...014a2d )
by Alberto
28s queued 13s
created

SchemaComponent::getSchema()   A

Complexity

Conditions 5
Paths 8

Size

Total Lines 32
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 17
c 0
b 0
f 0
nc 8
nop 2
dl 0
loc 32
rs 9.3888
1
<?php
2
/**
3
 * BEdita, API-first content management framework
4
 * Copyright 2018 ChannelWeb Srl, Chialab Srl
5
 *
6
 * This file is part of BEdita: you can redistribute it and/or modify
7
 * it under the terms of the GNU Lesser General Public License as published
8
 * by the Free Software Foundation, either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * See LICENSE.LGPL or <http://gnu.org/licenses/lgpl-3.0.html> for more details.
12
 */
13
namespace App\Controller\Component;
14
15
use BEdita\SDK\BEditaClientException;
16
use BEdita\WebTools\ApiClientProvider;
17
use Cake\Cache\Cache;
18
use Cake\Controller\Component;
19
use Cake\Core\Configure;
20
use Cake\Utility\Hash;
21
use Psr\Log\LogLevel;
22
23
/**
24
 * Handles JSON Schema of objects and resources.
25
 *
26
 * @property \Cake\Controller\Component\FlashComponent $Flash
27
 */
28
class SchemaComponent extends Component
29
{
30
    /**
31
     * {@inheritDoc}
32
     */
33
    public $components = ['Flash'];
34
35
    /**
36
     * Cache config name for type schemas.
37
     *
38
     * @var string
39
     */
40
    const CACHE_CONFIG = '_schema_types_';
41
42
    /**
43
     * {@inheritDoc}
44
     */
45
    protected $_defaultConfig = [
46
        'type' => null, // resource or object type name
47
        'internalSchema' => false, // use internal schema
48
    ];
49
50
    /**
51
     * Create multi project cache key.
52
     *
53
     * @param string $name Cache item name.
54
     * @return string
55
     */
56
    protected function cacheKey(string $name): string
57
    {
58
        $apiSignature = md5(ApiClientProvider::getApiClient()->getApiBaseUrl());
59
60
        return sprintf('%s_%s', $name, $apiSignature);
61
    }
62
63
    /**
64
     * Read type JSON Schema from API using internal cache.
65
     *
66
     * @param string|null $type Type to get schema for. By default, configured type is used.
67
     * @param string|null $revision Schema revision.
68
     * @return array|bool JSON Schema.
69
     */
70
    public function getSchema(string $type = null, string $revision = null)
71
    {
72
        if ($type === null) {
73
            $type = $this->getConfig('type');
74
        }
75
76
        if ($this->getConfig('internalSchema')) {
77
            return $this->loadInternalSchema($type);
78
        }
79
80
        $schema = $this->loadWithRevision($type, $revision);
81
        if ($schema !== false) {
82
            return $schema;
83
        }
84
85
        try {
86
            $schema = Cache::remember(
87
                $this->cacheKey($type),
88
                function () use ($type) {
89
                    return $this->fetchSchema($type);
90
                },
91
                self::CACHE_CONFIG
92
            );
93
        } catch (BEditaClientException $e) {
94
            // Something bad happened. Booleans **ARE** valid JSON Schemas: returning `false` instead.
95
            // The exception is being caught _outside_ of `Cache::remember()` to avoid caching the fallback.
96
            $this->log($e, LogLevel::ERROR);
97
98
            return false;
99
        }
100
101
        return $schema;
102
    }
103
104
    /**
105
     * Load schema from cache with revision check.
106
     * If cached revision don't match cache is removed.
107
     *
108
     * @param string $type Type to get schema for. By default, configured type is used.
109
     * @param string $revision Schema revision.
110
     * @return array|bool Cached schema if revision match, otherwise false
111
     */
112
    protected function loadWithRevision(string $type, string $revision = null)
113
    {
114
        $key = $this->cacheKey($type);
115
        $schema = Cache::read($key, self::CACHE_CONFIG);
116
        if ($schema === false) {
117
            return false;
118
        }
119
        $cacheRevision = empty($schema['revision']) ? null : $schema['revision'];
120
        if ($cacheRevision === $revision) {
121
            return $schema;
122
        }
123
        // remove from cache if revision don't match
124
        Cache::delete($key, self::CACHE_CONFIG);
125
126
        return false;
127
    }
128
129
    /**
130
     * Fetch JSON Schema via API.
131
     *
132
     * @param string $type Type to get schema for.
133
     * @return array|bool JSON Schema.
134
     */
135
    protected function fetchSchema(string $type)
136
    {
137
        $schema = ApiClientProvider::getApiClient()->schema($type);
138
        // add special property `roles` to `users`
139
        if ($type === 'users') {
140
            $schema['properties']['roles'] = [
141
                'type' => 'string',
142
                'enum' => $this->fetchRoles(),
143
            ];
144
        }
145
146
        return $schema;
147
    }
148
149
    /**
150
     * Fetch `roles` names
151
     *
152
     * @return array
153
     */
154
    protected function fetchRoles(): array
155
    {
156
        $query = [
157
            'fields' => 'name',
158
            'page_size' => 100,
159
        ];
160
        $response = ApiClientProvider::getApiClient()->get('/roles', $query);
161
162
        return (array)Hash::extract((array)$response, 'data.{n}.attributes.name');
163
    }
164
165
    /**
166
     * Load internal schema properties from configuration.
167
     *
168
     * @param string $type Resource type name
169
     * @return array
170
     */
171
    protected function loadInternalSchema(string $type): array
172
    {
173
        Configure::load('schema_properties');
174
        $properties = (array)Configure::read(sprintf('SchemaProperties.%s', $type), []);
175
176
        return compact('properties');
177
    }
178
179
    /**
180
     * Read relations schema from API using internal cache.
181
     *
182
     * @return array Relations schema.
183
     */
184
    public function getRelationsSchema()
185
    {
186
        try {
187
            $schema = (array)Cache::remember(
188
                $this->cacheKey('relations'),
189
                function () {
190
                    return $this->fetchRelationData();
191
                },
192
                self::CACHE_CONFIG
193
            );
194
        } catch (BEditaClientException $e) {
195
            // The exception is being caught _outside_ of `Cache::remember()` to avoid caching the fallback.
196
            $this->log($e, LogLevel::ERROR);
197
            $this->Flash->error($e->getMessage(), ['params' => $e]);
198
            $schema = [];
199
        }
200
201
        return $schema;
202
    }
203
204
    /**
205
     * Fetch relations schema via API.
206
     *
207
     * @return array Relations schema.
208
     */
209
    protected function fetchRelationData()
210
    {
211
        $query = [
212
            'include' => 'left_object_types,right_object_types',
213
            'page_size' => 100,
214
        ];
215
        $response = ApiClientProvider::getApiClient()->get('/model/relations', $query);
216
217
        $relations = [];
218
        // retrieve relation right and left object types
219
        $typeNames = Hash::combine((array)$response, 'included.{n}.id', 'included.{n}.attributes.name');
220
221
        foreach ($response['data'] as $res) {
222
            $leftTypes = (array)Hash::extract($res, 'relationships.left_object_types.data.{n}.id');
223
            $rightTypes = (array)Hash::extract($res, 'relationships.right_object_types.data.{n}.id');
224
            $res['left'] = array_values(array_intersect_key($typeNames, array_flip($leftTypes)));
225
            $res['right'] = array_values(array_intersect_key($typeNames, array_flip($rightTypes)));
226
            unset($res['relationships'], $res['links']);
227
            $relations[$res['attributes']['name']] = $res;
228
            $relations[$res['attributes']['inverse_name']] = $res;
229
        }
230
        Configure::load('relations');
231
232
        return $relations + Configure::read('DefaultRelations');
233
    }
234
}
235