| Total Complexity | 46 |
| Total Lines | 355 |
| Duplicated Lines | 0 % |
| Changes | 3 | ||
| Bugs | 3 | Features | 0 |
Complex classes like ModulesComponent often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use ModulesComponent, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 33 | class ModulesComponent extends Component |
||
| 34 | { |
||
| 35 | |||
| 36 | /** |
||
| 37 | * {@inheritDoc} |
||
| 38 | */ |
||
| 39 | public $components = ['Auth']; |
||
| 40 | |||
| 41 | /** |
||
| 42 | * {@inheritDoc} |
||
| 43 | */ |
||
| 44 | protected $_defaultConfig = [ |
||
| 45 | 'currentModuleName' => null, |
||
| 46 | 'clearHomeCache' => false, |
||
| 47 | ]; |
||
| 48 | |||
| 49 | /** |
||
| 50 | * Project modules for a user from `/home` endpoint |
||
| 51 | * |
||
| 52 | * @var array |
||
| 53 | */ |
||
| 54 | protected $modules = []; |
||
| 55 | |||
| 56 | /** |
||
| 57 | * Read modules and project info from `/home' endpoint. |
||
| 58 | * |
||
| 59 | * @return void |
||
| 60 | */ |
||
| 61 | public function startup(): void |
||
| 62 | { |
||
| 63 | if (empty($this->Auth->user('id'))) { |
||
| 64 | $this->getController()->set(['modules' => [], 'project' => []]); |
||
| 65 | |||
| 66 | return; |
||
| 67 | } |
||
| 68 | |||
| 69 | if ($this->getConfig('clearHomeCache')) { |
||
| 70 | Cache::delete(sprintf('home_%d', $this->Auth->user('id'))); |
||
| 71 | } |
||
| 72 | |||
| 73 | $modules = $this->getModules(); |
||
| 74 | $project = $this->getProject(); |
||
| 75 | $this->getController()->set(compact('modules', 'project')); |
||
| 76 | |||
| 77 | $currentModuleName = $this->getConfig('currentModuleName'); |
||
| 78 | if (!empty($currentModuleName)) { |
||
| 79 | $currentModule = Hash::get($modules, $currentModuleName); |
||
| 80 | } |
||
| 81 | |||
| 82 | if (!empty($currentModule)) { |
||
| 83 | $this->getController()->set(compact('currentModule')); |
||
| 84 | } |
||
| 85 | } |
||
| 86 | |||
| 87 | /** |
||
| 88 | * Getter for home endpoint metadata. |
||
| 89 | * |
||
| 90 | * @return array |
||
| 91 | */ |
||
| 92 | protected function getMeta(): array |
||
| 93 | { |
||
| 94 | try { |
||
| 95 | $home = Cache::remember( |
||
| 96 | sprintf('home_%d', $this->Auth->user('id')), |
||
| 97 | function () { |
||
| 98 | return ApiClientProvider::getApiClient()->get('/home'); |
||
| 99 | } |
||
| 100 | ); |
||
| 101 | } catch (BEditaClientException $e) { |
||
| 102 | // Something bad happened. Returning an empty array instead. |
||
| 103 | // The exception is being caught _outside_ of `Cache::remember()` to avoid caching the fallback. |
||
| 104 | $this->log($e, LogLevel::ERROR); |
||
| 105 | |||
| 106 | return []; |
||
| 107 | } |
||
| 108 | |||
| 109 | return !empty($home['meta']) ? $home['meta'] : []; |
||
| 110 | } |
||
| 111 | |||
| 112 | /** |
||
| 113 | * Create internal list of available modules in `$this->modules` as an array with `name` as key |
||
| 114 | * and return it. |
||
| 115 | * Modules are read from `/home` endpoint |
||
| 116 | * |
||
| 117 | * @return array |
||
| 118 | */ |
||
| 119 | public function getModules(): array |
||
| 120 | { |
||
| 121 | $modulesOrder = Configure::read('Modules.order'); |
||
| 122 | |||
| 123 | $meta = $this->getMeta(); |
||
| 124 | $modules = collection(Hash::get($meta, 'resources', [])) |
||
| 125 | ->map(function (array $data, $endpoint) { |
||
| 126 | $name = substr($endpoint, 1); |
||
| 127 | |||
| 128 | return $data + compact('name'); |
||
| 129 | }) |
||
| 130 | ->reject(function (array $data) { |
||
| 131 | return Hash::get($data, 'hints.object_type') !== true && Hash::get($data, 'name') !== 'trash'; |
||
| 132 | }) |
||
| 133 | ->sortBy(function (array $data) use ($modulesOrder) { |
||
| 134 | $name = Hash::get($data, 'name'); |
||
| 135 | $idx = array_search($name, $modulesOrder); |
||
| 136 | if ($idx === false) { |
||
| 137 | // No configured order for this module. Use hash to preserve order, and ensure it is after other modules. |
||
| 138 | $idx = count($modulesOrder) + hexdec(hash('crc32', $name)); |
||
| 139 | |||
| 140 | if ($name === 'trash') { |
||
| 141 | // Trash eventually. |
||
| 142 | $idx = PHP_INT_MAX; |
||
| 143 | } |
||
| 144 | } |
||
| 145 | |||
| 146 | return -$idx; |
||
| 147 | }) |
||
| 148 | ->toList(); |
||
| 149 | $plugins = Configure::read('Modules.plugins'); |
||
| 150 | if ($plugins) { |
||
| 151 | $modules = array_merge($modules, $plugins); |
||
| 152 | } |
||
| 153 | $this->modules = Hash::combine($modules, '{n}.name', '{n}'); |
||
| 154 | |||
| 155 | return $this->modules; |
||
| 156 | } |
||
| 157 | |||
| 158 | /** |
||
| 159 | * Get information about current project. |
||
| 160 | * |
||
| 161 | * @return array |
||
| 162 | */ |
||
| 163 | public function getProject(): array |
||
| 164 | { |
||
| 165 | $meta = $this->getMeta(); |
||
| 166 | $project = [ |
||
| 167 | 'name' => Hash::get($meta, 'project.name', ''), |
||
| 168 | 'version' => Hash::get($meta, 'version', ''), |
||
| 169 | 'colophon' => '', // TODO: populate this value. |
||
| 170 | ]; |
||
| 171 | |||
| 172 | return $project; |
||
| 173 | } |
||
| 174 | |||
| 175 | /** |
||
| 176 | * Check if an object type is abstract or concrete. |
||
| 177 | * This method MUST NOT be called from `beforeRender` since `$this->modules` array is still not initialized. |
||
| 178 | * |
||
| 179 | * @param string $name Name of object type. |
||
| 180 | * @return bool True if abstract, false if concrete |
||
| 181 | */ |
||
| 182 | public function isAbstract(string $name): bool |
||
| 183 | { |
||
| 184 | return (bool)Hash::get($this->modules, sprintf('%s.hints.multiple_types', $name), false); |
||
| 185 | } |
||
| 186 | |||
| 187 | /** |
||
| 188 | * Get list of object types |
||
| 189 | * This method MUST NOT be called from `beforeRender` since `$this->modules` array is still not initialized. |
||
| 190 | * |
||
| 191 | * @param bool|null $abstract Only abstract or concrete types. |
||
| 192 | * @return array Type names list |
||
| 193 | */ |
||
| 194 | public function objectTypes(?bool $abstract = null): array |
||
| 195 | { |
||
| 196 | $types = []; |
||
| 197 | foreach ($this->modules as $name => $data) { |
||
| 198 | if (!$data['hints']['object_type']) { |
||
| 199 | continue; |
||
| 200 | } |
||
| 201 | if ($abstract === null || $data['hints']['multiple_types'] === $abstract) { |
||
| 202 | $types[] = $name; |
||
| 203 | } |
||
| 204 | } |
||
| 205 | |||
| 206 | return $types; |
||
| 207 | } |
||
| 208 | |||
| 209 | /** |
||
| 210 | * Read oEmbed metadata |
||
| 211 | * |
||
| 212 | * @param string $url Remote URL |
||
| 213 | * @return array|null |
||
| 214 | * @codeCoverageIgnore |
||
| 215 | */ |
||
| 216 | protected function oEmbedMeta(string $url): ?array |
||
| 217 | { |
||
| 218 | return (new OEmbed())->readMetadata($url); |
||
| 219 | } |
||
| 220 | |||
| 221 | /** |
||
| 222 | * Upload a file and store it in a media stream |
||
| 223 | * Or create a remote media trying to get some metadata via oEmbed |
||
| 224 | * |
||
| 225 | * @param array $requestData The request data from form |
||
| 226 | * @return void |
||
| 227 | */ |
||
| 228 | public function upload(array &$requestData): void |
||
| 229 | { |
||
| 230 | $uploadBehavior = Hash::get($requestData, 'upload_behavior', 'file'); |
||
| 231 | |||
| 232 | if ($uploadBehavior === 'embed' && !empty($requestData['remote_url'])) { |
||
| 233 | $data = $this->oEmbedMeta($requestData['remote_url']); |
||
| 234 | $requestData = array_filter($requestData) + $data; |
||
| 235 | |||
| 236 | return; |
||
| 237 | } |
||
| 238 | if (empty($requestData['file'])) { |
||
| 239 | return; |
||
| 240 | } |
||
| 241 | |||
| 242 | // verify upload form data |
||
| 243 | if ($this->checkRequestForUpload($requestData)) { |
||
| 244 | // has another stream? drop it |
||
| 245 | $this->removeStream($requestData); |
||
| 246 | |||
| 247 | // upload file |
||
| 248 | $filename = $requestData['file']['name']; |
||
| 249 | $filepath = $requestData['file']['tmp_name']; |
||
| 250 | $headers = ['Content-Type' => $requestData['file']['type']]; |
||
| 251 | $apiClient = ApiClientProvider::getApiClient(); |
||
| 252 | $response = $apiClient->upload($filename, $filepath, $headers); |
||
| 253 | |||
| 254 | // assoc stream to media |
||
| 255 | $streamId = $response['data']['id']; |
||
| 256 | $requestData['id'] = $this->assocStreamToMedia($streamId, $requestData, $filename); |
||
| 257 | } |
||
| 258 | unset($requestData['file'], $requestData['remote_url']); |
||
| 259 | } |
||
| 260 | |||
| 261 | /** |
||
| 262 | * Remove a stream from a media, if any |
||
| 263 | * |
||
| 264 | * @param array $requestData The request data from form |
||
| 265 | * @return void |
||
| 266 | */ |
||
| 267 | public function removeStream(array $requestData): void |
||
| 280 | } |
||
| 281 | |||
| 282 | /** |
||
| 283 | * Associate a stream to a media using API |
||
| 284 | * If $requestData['id'] is null, create media from stream. |
||
| 285 | * If $requestData['id'] is not null, replace properly related stream. |
||
| 286 | * |
||
| 287 | * @param string $streamId The stream ID |
||
| 288 | * @param array $requestData The request data |
||
| 289 | * @param string $defaultTitle The default title for media |
||
| 290 | * @return string The media ID |
||
| 291 | */ |
||
| 292 | public function assocStreamToMedia(string $streamId, array &$requestData, string $defaultTitle): string |
||
| 293 | { |
||
| 294 | $apiClient = ApiClientProvider::getApiClient(); |
||
| 295 | $type = $requestData['model-type']; |
||
| 296 | if (empty($requestData['id'])) { |
||
| 297 | // create media from stream |
||
| 298 | // save only `title` (filename if not set) and `status` in new media object |
||
| 299 | $attributes = array_filter([ |
||
| 300 | 'title' => !empty($requestData['title']) ? $requestData['title'] : $defaultTitle, |
||
| 301 | 'status' => Hash::get($requestData, 'status'), |
||
| 302 | ]); |
||
| 303 | $data = compact('type', 'attributes'); |
||
| 304 | $body = compact('data'); |
||
| 305 | $response = $apiClient->createMediaFromStream($streamId, $type, $body); |
||
| 306 | // `title` and `status` saved here, remove from next save |
||
| 307 | unset($requestData['title'], $requestData['status']); |
||
| 308 | |||
| 309 | return $response['data']['id']; |
||
| 310 | } |
||
| 311 | |||
| 312 | // assoc existing media to stream |
||
| 313 | $id = $requestData['id']; |
||
| 314 | $data = compact('id', 'type'); |
||
| 315 | $apiClient->replaceRelated($streamId, 'streams', 'object', $data); |
||
| 316 | |||
| 317 | return $id; |
||
| 318 | } |
||
| 319 | |||
| 320 | /** |
||
| 321 | * Check request data for upload and return true if upload is boht possible and needed |
||
| 322 | * |
||
| 323 | * |
||
| 324 | * @param array $requestData The request data |
||
| 325 | * @return bool true if upload is possible and needed |
||
| 326 | */ |
||
| 327 | public function checkRequestForUpload(array $requestData): bool |
||
| 351 | } |
||
| 352 | |||
| 353 | /** |
||
| 354 | * Update object, when failed save occurred. |
||
| 355 | * Check session data by `failedSave.{type}.{id}` key and `failedSave.{type}.{id}__timestamp`. |
||
| 356 | * If data is set and timestamp is not older than 5 minutes. |
||
| 357 | * |
||
| 358 | * @param array $object The object. |
||
| 359 | * @return void |
||
| 360 | */ |
||
| 361 | public function updateFromFailedSave(array &$object): void |
||
| 390 |
This property has been deprecated. The supplier of the class has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.