| Total Complexity | 71 | 
| Total Lines | 530 | 
| Duplicated Lines | 0 % | 
| Changes | 4 | ||
| Bugs | 2 | Features | 0 | 
Complex classes like Config 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 Config, and based on these observations, apply Extract Interface, too.
| 1 | <?php | ||
| 23 | class Config | ||
| 24 | { | ||
| 25 | /** @var Data Configuration is a Data object. */ | ||
| 26 | protected $data; | ||
| 27 | |||
| 28 | /** @var array Configuration. */ | ||
| 29 | protected $siteConfig; | ||
| 30 | |||
| 31 | /** @var string Source directory. */ | ||
| 32 | protected $sourceDir; | ||
| 33 | |||
| 34 | /** @var string Destination directory. */ | ||
| 35 | protected $destinationDir; | ||
| 36 | |||
| 37 | /** @var array Languages. */ | ||
| 38 | protected $languages = null; | ||
| 39 | |||
| 40 |     const LANG_CODE_PATTERN = '([a-z]{2}(-[A-Z]{2})?)'; // "fr" or "fr-FR" | ||
| 41 |     const LANG_LOCALE_PATTERN = '[a-z]{2}(_[A-Z]{2})?(_[A-Z]{2})?'; // "fr" or "fr_FR" or "no_NO_NY" | ||
| 42 | |||
| 43 | /** | ||
| 44 | * Build the Config object with the default config + the optional given array. | ||
| 45 | */ | ||
| 46 | public function __construct(?array $config = null) | ||
| 47 |     { | ||
| 48 | // load default configuration | ||
| 49 | $defaultConfig = realpath(Util::joinFile(__DIR__, '..', 'config/default.php')); | ||
| 50 |         if (Plateform::isPhar()) { | ||
| 51 | $defaultConfig = Util::joinPath(Plateform::getPharPath(), 'config/default.php'); | ||
| 52 | } | ||
| 53 | $this->data = new Data(include $defaultConfig); | ||
| 54 | |||
| 55 | // import site config | ||
| 56 | $this->siteConfig = $config; | ||
| 57 | $this->importSiteConfig(); | ||
| 58 | } | ||
| 59 | |||
| 60 | /** | ||
| 61 | * Imports site configuration. | ||
| 62 | */ | ||
| 63 | private function importSiteConfig(): void | ||
| 64 |     { | ||
| 65 | $this->data->import($this->siteConfig); | ||
| 66 | |||
| 67 | /** | ||
| 68 | * Overrides configuration with environment variables. | ||
| 69 | */ | ||
| 70 | $data = $this->getData(); | ||
| 71 |         $applyEnv = function ($array) use ($data) { | ||
| 72 | $iterator = new \RecursiveIteratorIterator( | ||
| 73 | new \RecursiveArrayIterator($array), | ||
| 74 | \RecursiveIteratorIterator::SELF_FIRST | ||
| 75 | ); | ||
| 76 | $iterator->rewind(); | ||
| 77 |             while ($iterator->valid()) { | ||
| 78 | $path = []; | ||
| 79 |                 foreach (range(0, $iterator->getDepth()) as $depth) { | ||
| 80 | $path[] = $iterator->getSubIterator($depth)->key(); | ||
| 81 | } | ||
| 82 |                 $sPath = implode('_', $path); | ||
| 83 |                 if ($getEnv = getenv('CECIL_'.strtoupper($sPath))) { | ||
| 84 |                     $data->set(str_replace('_', '.', strtolower($sPath)), $this->castSetValue($getEnv)); | ||
| 85 | } | ||
| 86 | $iterator->next(); | ||
| 87 | } | ||
| 88 | }; | ||
| 89 | $applyEnv($data->export()); | ||
| 90 | } | ||
| 91 | |||
| 92 | /** | ||
| 93 | * Casts boolean value given to set() as string. | ||
| 94 | * | ||
| 95 | * @param mixed $value | ||
| 96 | * | ||
| 97 | * @return bool|mixed | ||
| 98 | */ | ||
| 99 | private function castSetValue($value) | ||
| 100 |     { | ||
| 101 |         if (is_string($value)) { | ||
| 102 |             switch ($value) { | ||
| 103 | case 'true': | ||
| 104 | return true; | ||
| 105 | case 'false': | ||
| 106 | return false; | ||
| 107 | default: | ||
| 108 | return $value; | ||
| 109 | } | ||
| 110 | } | ||
| 111 | |||
| 112 | return $value; | ||
| 113 | } | ||
| 114 | |||
| 115 | /** | ||
| 116 | * Imports (theme) configuration. | ||
| 117 | */ | ||
| 118 | public function import(array $config): void | ||
| 119 |     { | ||
| 120 | $this->data->import($config); | ||
| 121 | |||
| 122 | // re-import site config | ||
| 123 | $this->importSiteConfig(); | ||
| 124 | } | ||
| 125 | |||
| 126 | /** | ||
| 127 | * Set a Data object as configuration. | ||
| 128 | */ | ||
| 129 | protected function setData(Data $data): self | ||
| 130 |     { | ||
| 131 |         if ($this->data !== $data) { | ||
| 132 | $this->data = $data; | ||
| 133 | } | ||
| 134 | |||
| 135 | return $this; | ||
| 136 | } | ||
| 137 | |||
| 138 | /** | ||
| 139 | * Get configuration as a Data object. | ||
| 140 | */ | ||
| 141 | protected function getData(): Data | ||
| 144 | } | ||
| 145 | |||
| 146 | /** | ||
| 147 | * Get configuration as an array. | ||
| 148 | */ | ||
| 149 | public function getAsArray(): array | ||
| 150 |     { | ||
| 151 | return $this->data->export(); | ||
| 152 | } | ||
| 153 | |||
| 154 | /** | ||
| 155 | * Is configuration's key exists? | ||
| 156 | */ | ||
| 157 | public function has(string $key): bool | ||
| 158 |     { | ||
| 159 | return $this->data->has($key); | ||
| 160 | } | ||
| 161 | |||
| 162 | /** | ||
| 163 | * Get the value of a configuration's key. | ||
| 164 | * | ||
| 165 | * @param string $key Configuration key | ||
| 166 | * @param string $language Language code (optionnal) | ||
| 167 | * @param bool $fallback Set to false to not return the value in the default language as fallback | ||
| 168 | * | ||
| 169 | * @return mixed|null | ||
| 170 | */ | ||
| 171 | public function get(string $key, ?string $language = null, bool $fallback = true) | ||
| 172 |     { | ||
| 173 |         if ($language !== null) { | ||
| 174 | $index = $this->getLanguageIndex($language); | ||
| 175 |             $keyLang = \sprintf('languages.%s.config.%s', $index, $key); | ||
| 176 |             if ($this->data->has($keyLang)) { | ||
| 177 | return $this->data->get($keyLang); | ||
| 178 | } | ||
| 179 |             if ($language !== $this->getLanguageDefault() && $fallback === false) { | ||
| 180 | return null; | ||
| 181 | } | ||
| 182 | } | ||
| 183 | |||
| 184 |         if ($this->data->has($key)) { | ||
| 185 | return $this->data->get($key); | ||
| 186 | } | ||
| 187 | |||
| 188 | return null; | ||
| 189 | } | ||
| 190 | |||
| 191 | /** | ||
| 192 | * Set the source directory. | ||
| 193 | * | ||
| 194 | * @throws \InvalidArgumentException | ||
| 195 | */ | ||
| 196 | public function setSourceDir(string $sourceDir = null): self | ||
| 197 |     { | ||
| 198 |         if ($sourceDir === null) { | ||
| 199 | $sourceDir = getcwd(); | ||
| 200 | } | ||
| 201 |         if (!is_dir($sourceDir)) { | ||
| 202 |             throw new \InvalidArgumentException(\sprintf('The directory "%s" is not a valid source!', $sourceDir)); | ||
| 203 | } | ||
| 204 | $this->sourceDir = $sourceDir; | ||
| 205 | |||
| 206 | return $this; | ||
| 207 | } | ||
| 208 | |||
| 209 | /** | ||
| 210 | * Get the source directory. | ||
| 211 | */ | ||
| 212 | public function getSourceDir(): string | ||
| 213 |     { | ||
| 214 | return $this->sourceDir; | ||
| 215 | } | ||
| 216 | |||
| 217 | /** | ||
| 218 | * Set the destination directory. | ||
| 219 | * | ||
| 220 | * @throws \InvalidArgumentException | ||
| 221 | */ | ||
| 222 | public function setDestinationDir(string $destinationDir = null): self | ||
| 236 | } | ||
| 237 | |||
| 238 | /** | ||
| 239 | * Get the destination directory. | ||
| 240 | */ | ||
| 241 | public function getDestinationDir(): string | ||
| 242 |     { | ||
| 243 | return $this->destinationDir; | ||
| 244 | } | ||
| 245 | |||
| 246 | /** | ||
| 247 | * Path helpers. | ||
| 248 | */ | ||
| 249 | |||
| 250 | /** | ||
| 251 | * Returns the path of the pages directory. | ||
| 252 | */ | ||
| 253 | public function getPagesPath(): string | ||
| 254 |     { | ||
| 255 |         $path = Util::joinFile($this->getSourceDir(), (string) $this->get('pages.dir')); | ||
| 256 | |||
| 257 | // legacy support | ||
| 258 |         if (!is_dir($path)) { | ||
| 259 | $path = Util::joinFile($this->getSourceDir(), 'content'); | ||
| 260 | } | ||
| 261 | |||
| 262 | return $path; | ||
| 263 | } | ||
| 264 | |||
| 265 | /** | ||
| 266 | * Returns the path of the data directory. | ||
| 267 | */ | ||
| 268 | public function getDataPath(): string | ||
| 269 |     { | ||
| 270 |         return Util::joinFile($this->getSourceDir(), (string) $this->get('data.dir')); | ||
| 271 | } | ||
| 272 | |||
| 273 | /** | ||
| 274 | * Returns the path of templates directory. | ||
| 275 | */ | ||
| 276 | public function getLayoutsPath(): string | ||
| 277 |     { | ||
| 278 |         return Util::joinFile($this->getSourceDir(), (string) $this->get('layouts.dir')); | ||
| 279 | } | ||
| 280 | |||
| 281 | /** | ||
| 282 | * Returns the path of themes directory. | ||
| 283 | */ | ||
| 284 | public function getThemesPath(): string | ||
| 285 |     { | ||
| 286 |         return Util::joinFile($this->getSourceDir(), (string) $this->get('themes.dir')); | ||
| 287 | } | ||
| 288 | |||
| 289 | /** | ||
| 290 | * Returns the path of internal templates directory. | ||
| 291 | */ | ||
| 292 | public function getInternalLayoutsPath(): string | ||
| 295 | } | ||
| 296 | |||
| 297 | /** | ||
| 298 | * Returns the path of translations directory. | ||
| 299 | */ | ||
| 300 | public function getTranslationsPath(): string | ||
| 301 |     { | ||
| 302 |         return Util::joinFile($this->getSourceDir(), (string) $this->get('translations.dir')); | ||
| 303 | } | ||
| 304 | |||
| 305 | /** | ||
| 306 | * Returns the path of internal translations directory. | ||
| 307 | */ | ||
| 308 | public function getInternalTranslationsPath(): string | ||
| 309 |     { | ||
| 310 |         return Util::joinPath(__DIR__, '..', (string) $this->get('translations.internal.dir')); | ||
| 311 | } | ||
| 312 | |||
| 313 | /** | ||
| 314 | * Returns the path of the output directory. | ||
| 315 | */ | ||
| 316 | public function getOutputPath(): string | ||
| 317 |     { | ||
| 318 |         return Util::joinFile($this->getDestinationDir(), (string) $this->get('output.dir')); | ||
| 319 | } | ||
| 320 | |||
| 321 | /** | ||
| 322 | * Returns the path of static files directory. | ||
| 323 | */ | ||
| 324 | public function getStaticPath(): string | ||
| 325 |     { | ||
| 326 |         return Util::joinFile($this->getSourceDir(), (string) $this->get('static.dir')); | ||
| 327 | } | ||
| 328 | |||
| 329 | /** | ||
| 330 | * Returns the path of static files directory, with a target. | ||
| 331 | */ | ||
| 332 | public function getStaticTargetPath(): string | ||
| 333 |     { | ||
| 334 | $path = $this->getStaticPath(); | ||
| 335 | |||
| 336 |         if (!empty($this->get('static.target'))) { | ||
| 337 |             $path = substr($path, 0, -strlen((string) $this->get('static.target'))); | ||
| 338 | } | ||
| 339 | |||
| 340 | return $path; | ||
| 341 | } | ||
| 342 | |||
| 343 | /** | ||
| 344 | * Returns the path of assets files directory. | ||
| 345 | */ | ||
| 346 | public function getAssetsPath(): string | ||
| 347 |     { | ||
| 348 |         return Util::joinFile($this->getSourceDir(), (string) $this->get('assets.dir')); | ||
| 349 | } | ||
| 350 | |||
| 351 | /** | ||
| 352 | * Is cache dir is absolute to system files | ||
| 353 | * or relative to project destination? | ||
| 354 | */ | ||
| 355 | public function isCacheDirIsAbsolute(): bool | ||
| 356 |     { | ||
| 357 |         $path = (string) $this->get('cache.dir'); | ||
| 358 |         if (Util::joinFile($path) == realpath(Util::joinFile($path))) { | ||
| 359 | return true; | ||
| 360 | } | ||
| 361 | |||
| 362 | return false; | ||
| 363 | } | ||
| 364 | |||
| 365 | /** | ||
| 366 | * Returns cache path. | ||
| 367 | * | ||
| 368 | * @throws RuntimeException | ||
| 369 | */ | ||
| 370 | public function getCachePath(): string | ||
| 371 |     { | ||
| 372 |         if (empty((string) $this->get('cache.dir'))) { | ||
| 373 |             throw new RuntimeException(\sprintf('The cache directory ("%s") is not defined in configuration.', 'cache.dir')); | ||
| 374 | } | ||
| 375 | |||
| 376 |         if ($this->isCacheDirIsAbsolute()) { | ||
| 377 |             $cacheDir = Util::joinFile((string) $this->get('cache.dir'), 'cecil'); | ||
| 378 | Util\File::getFS()->mkdir($cacheDir); | ||
| 379 | |||
| 380 | return $cacheDir; | ||
| 381 | } | ||
| 382 | |||
| 383 |         return Util::joinFile($this->getDestinationDir(), (string) $this->get('cache.dir')); | ||
| 384 | } | ||
| 385 | |||
| 386 | /** | ||
| 387 | * Returns cache path of templates. | ||
| 388 | */ | ||
| 389 | public function getCacheTemplatesPath(): string | ||
| 390 |     { | ||
| 391 |         return Util::joinFile($this->getCachePath(), (string) $this->get('cache.templates.dir')); | ||
| 392 | } | ||
| 393 | |||
| 394 | /** | ||
| 395 | * Returns cache path of translations. | ||
| 396 | */ | ||
| 397 | public function getCacheTranslationsPath(): string | ||
| 398 |     { | ||
| 399 |         return Util::joinFile($this->getCachePath(), (string) $this->get('cache.translations.dir')); | ||
| 400 | } | ||
| 401 | |||
| 402 | /** | ||
| 403 | * Returns cache path of remote assets. | ||
| 404 | */ | ||
| 405 | public function getCacheAssetsPath(): string | ||
| 406 |     { | ||
| 407 |         return Util::joinFile($this->getCachePath(), (string) $this->get('cache.assets.dir')); | ||
| 408 | } | ||
| 409 | |||
| 410 | /** | ||
| 411 | * Returns the property value of an output format. | ||
| 412 | * | ||
| 413 | * @throws RuntimeException | ||
| 414 | * | ||
| 415 | * @return string|array|null | ||
| 416 | */ | ||
| 417 | public function getOutputFormatProperty(string $name, string $property) | ||
| 418 |     { | ||
| 419 |         $properties = array_column((array) $this->get('output.formats'), $property, 'name'); | ||
| 420 | |||
| 421 |         if (empty($properties)) { | ||
| 422 |             throw new RuntimeException(\sprintf('Property "%s" is not defined for format "%s".', $property, $name)); | ||
| 423 | } | ||
| 424 | |||
| 425 | return $properties[$name] ?? null; | ||
| 426 | } | ||
| 427 | |||
| 428 | /** | ||
| 429 | * Theme helpers. | ||
| 430 | */ | ||
| 431 | |||
| 432 | /** | ||
| 433 | * Returns theme(s) as an array. | ||
| 434 | */ | ||
| 435 | public function getTheme(): ?array | ||
| 436 |     { | ||
| 437 |         if ($themes = $this->get('theme')) { | ||
| 438 |             if (is_array($themes)) { | ||
| 439 | return $themes; | ||
| 440 | } | ||
| 441 | |||
| 442 | return [$themes]; | ||
| 443 | } | ||
| 444 | |||
| 445 | return null; | ||
| 446 | } | ||
| 447 | |||
| 448 | /** | ||
| 449 | * Has a (valid) theme(s)? | ||
| 450 | * | ||
| 451 | * @throws RuntimeException | ||
| 452 | */ | ||
| 453 | public function hasTheme(): bool | ||
| 454 |     { | ||
| 455 |         if ($themes = $this->getTheme()) { | ||
| 456 |             foreach ($themes as $theme) { | ||
| 457 |                 if (!Util\File::getFS()->exists($this->getThemeDirPath($theme, 'layouts'))) { | ||
| 458 |                     throw new RuntimeException(\sprintf('Theme directory "%s" not found!', Util::joinFile($this->getThemesPath(), $theme, 'layouts'))); | ||
| 459 | } | ||
| 460 | } | ||
| 461 | |||
| 462 | return true; | ||
| 463 | } | ||
| 464 | |||
| 465 | return false; | ||
| 466 | } | ||
| 467 | |||
| 468 | /** | ||
| 469 | * Returns the path of a specific theme's directory. | ||
| 470 |      * ("layouts" by default). | ||
| 471 | */ | ||
| 472 | public function getThemeDirPath(string $theme, string $dir = 'layouts'): string | ||
| 475 | } | ||
| 476 | |||
| 477 | /** | ||
| 478 | * Language helpers. | ||
| 479 | */ | ||
| 480 | |||
| 481 | /** | ||
| 482 | * Returns an array of available languages. | ||
| 483 | * | ||
| 484 | * @throws RuntimeException | ||
| 485 | */ | ||
| 486 | public function getLanguages(): array | ||
| 487 |     { | ||
| 488 |         if ($this->languages !== null) { | ||
| 489 | return $this->languages; | ||
| 490 | } | ||
| 491 | |||
| 492 |         $languages = (array) $this->get('languages'); | ||
| 493 | |||
| 494 |         if (!is_int(array_search($this->getLanguageDefault(), array_column($languages, 'code')))) { | ||
| 495 |             throw new RuntimeException(\sprintf('The default language "%s" is not listed in "languages" key configuration.', $this->getLanguageDefault())); | ||
| 496 | } | ||
| 497 | |||
| 498 |         $languages = array_filter($languages, function ($language) { | ||
| 499 | return !(isset($language['enabled']) && $language['enabled'] === false); | ||
| 500 | }); | ||
| 501 | |||
| 502 | $this->languages = $languages; | ||
| 503 | |||
| 504 | return $this->languages; | ||
| 505 | } | ||
| 506 | |||
| 507 | /** | ||
| 508 | * Returns the default language code (ie: "en", "fr-FR", etc.). | ||
| 509 | * | ||
| 510 | * @throws RuntimeException | ||
| 511 | */ | ||
| 512 | public function getLanguageDefault(): string | ||
| 513 |     { | ||
| 514 |         if (!$this->get('language')) { | ||
| 515 |             throw new RuntimeException('There is no default "language" key in configuration.'); | ||
| 516 | } | ||
| 517 | |||
| 518 |         return $this->get('language'); | ||
| 519 | } | ||
| 520 | |||
| 521 | /** | ||
| 522 | * Returns a language code index. | ||
| 523 | * | ||
| 524 | * @throws RuntimeException | ||
| 525 | */ | ||
| 526 | public function getLanguageIndex(string $code): int | ||
| 535 | } | ||
| 536 | |||
| 537 | /** | ||
| 538 | * Returns the property value of a (specified or default) language. | ||
| 539 | * | ||
| 540 | * @throws RuntimeException | ||
| 541 | */ | ||
| 542 | public function getLanguageProperty(string $property, ?string $code = null): string | ||
| 553 | } | ||
| 554 | } | ||
| 555 |