file ProviderBase.php

  1. 8.x-3.x src/Plugin/Provider/ProviderBase.php
  2. 7.x-3.x includes/cdn/ProviderBase.php

Namespace

Drupal\bootstrap\Plugin\Provider
  1. <?php
  2. namespace Drupal\bootstrap\Plugin\Provider;
  3. use Drupal\bootstrap\Bootstrap;
  4. use Drupal\bootstrap\Plugin\PluginBase;
  5. use Drupal\bootstrap\Plugin\ProviderManager;
  6. use Drupal\bootstrap\Utility\Crypt;
  7. use Drupal\bootstrap\Utility\Unicode;
  8. use Drupal\Component\Serialization\Json;
  9. use Drupal\Component\Utility\NestedArray;
  10. /**
  11. * CDN Provider base class.
  12. *
  13. * @ingroup plugins_provider
  14. */
  15. class ProviderBase extends PluginBase implements ProviderInterface {
  16. /**
  17. * The currently set assets.
  18. *
  19. * @var array
  20. *
  21. * @deprecated in 8.x-3.18, will be removed in a future release.
  22. */
  23. protected $assets = [];
  24. /**
  25. * The cache backend used for storing various permanent CDN Provider data.
  26. *
  27. * @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface
  28. */
  29. protected $keyValue;
  30. /**
  31. * The cache backend used for storing various expirable CDN Provider data.
  32. *
  33. * @var \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface
  34. */
  35. protected $keyValueExpirable;
  36. /**
  37. * The cache TTL values, in seconds, keyed by type.
  38. *
  39. * @var int[]
  40. *
  41. * @see \Drupal\bootstrap\Plugin\Provider\ProviderInterface
  42. */
  43. protected $cacheTtl = [];
  44. /**
  45. * The currently set CDN assets, keyed by a hash identifier.
  46. *
  47. * @var \Drupal\bootstrap\Plugin\Provider\CdnAssets[]
  48. */
  49. protected $cdnAssets;
  50. /**
  51. * A list of currently set Exception objects.
  52. *
  53. * @var \Drupal\bootstrap\Plugin\Provider\ProviderException[]
  54. */
  55. protected $cdnExceptions = [];
  56. /**
  57. * The versions supplied by the CDN Provider.
  58. *
  59. * @var array
  60. */
  61. protected $versions;
  62. /**
  63. * The themes supplied by the CDN Provider, keyed by version.
  64. *
  65. * @var array[]
  66. */
  67. protected $themes = [];
  68. /**
  69. * Adds a new CDN Provider exception.
  70. *
  71. * @param \Throwable $exception
  72. * The exception message.
  73. */
  74. protected function addCdnException(\Throwable $exception) {
  75. $this->cdnExceptions[] = new ProviderException($this, $exception->getMessage(), $exception->getCode(), $exception);
  76. }
  77. /**
  78. * {@inheritdoc}
  79. *
  80. * @see \Drupal\bootstrap\Plugin\Provider\ProviderBase::getCdnAssetsCacheData()
  81. */
  82. public function alterFrameworkLibrary(array &$framework) {
  83. // Attempt to retrieve cached CDN assets from the database. This is
  84. // primarily used to avoid unnecessary API requests and speed up the
  85. // process during a cache rebuild. The "keyvalue.expirable" service is
  86. // used as it persists through cache rebuilds. In order to prevent stale
  87. // data, a hash is used constructed of various data relating to the CDN.
  88. // The cache is rebuilt if and when it has expired.
  89. // @see https://www.drupal.org/project/bootstrap/issues/3031415
  90. $data = $this->getCdnAssetsCacheData();
  91. $hash = Crypt::generateBase64HashIdentifier($data);
  92. // Retrieve the cached value or build it if necessary.
  93. $framework = $this->cacheGet('library', $hash, [], function () use ($framework, $data) {
  94. $version = isset($data['version']) ? $data['version'] : NULL;
  95. $theme = isset($data['theme']) ? $data['theme'] : NULL;
  96. $assets = $this->getCdnAssets($version, $theme)->toLibraryArray($data['min']);
  97. // Immediately return if there are no theme CDN assets to use.
  98. if (empty($assets)) {
  99. return $framework;
  100. }
  101. // Override the framework version with the CDN version that is being used.
  102. if (isset($data['version'])) {
  103. $framework['version'] = $data['version'];
  104. }
  105. // @todo Provide a UI setting for this?
  106. $styles = [];
  107. if ($this->theme->getSetting('cdn_styles', TRUE)) {
  108. $stylesProvider = ProviderManager::load($this->theme, 'drupal_bootstrap_styles');
  109. $styles = $stylesProvider->getCdnAssets($version, $theme)->toLibraryArray($data['min']);
  110. }
  111. // Merge the assets with the existing library info and return it.
  112. return NestedArray::mergeDeepArray([$assets, $styles, $framework], TRUE);
  113. });
  114. }
  115. /**
  116. * Retrieves a value from the CDN Provider cache.
  117. *
  118. * @param string $type
  119. * The type of cache item to retrieve.
  120. * @param string $key
  121. * Optional. A specific key of the item to retrieve. Note: this can be in
  122. * the form of dot notation if the value is nested in an array. If not
  123. * provided, the entire contents of $name will be returned.
  124. * @param mixed $default
  125. * Optional. The default value to return if $key is not set.
  126. * @param callable $builder
  127. * Optional. If provided, a builder will be invoked when there is no cache
  128. * currently set. The return value of the build will be used to set the
  129. * cached value, provided there are no CDN Provider exceptions generated.
  130. * If there are, but you still need the cache to be set, reset them prior
  131. * to returning from the builder callback.
  132. *
  133. * @return mixed
  134. * The cached value if it's set or the value supplied to $default if not.
  135. */
  136. protected function cacheGet($type, $key = NULL, $default = NULL, callable $builder = NULL) {
  137. $ttl = $this->getCacheTtl($type);
  138. $never = $ttl === static::TTL_NEVER;
  139. $forever = $ttl === static::TTL_FOREVER;
  140. $cache = $forever ? $this->getKeyValue() : $this->getKeyValueExpirable();
  141. $data = $cache->get($type, []);
  142. if (!isset($key)) {
  143. return $data;
  144. }
  145. $parts = Unicode::splitDelimiter($key);
  146. $value = NestedArray::getValue($data, $parts, $key_exists);
  147. // Build the cache.
  148. if (!$key_exists && $builder) {
  149. $value = $builder($default);
  150. if (!isset($value)) {
  151. $value = $default;
  152. }
  153. NestedArray::setValue($data, $parts, $value);
  154. // Only set the cache if no CDN Provider exceptions were thrown.
  155. if (!$this->cdnExceptions && !$never) {
  156. if ($forever) {
  157. $cache->set($type, $data);
  158. }
  159. else {
  160. $cache->setWithExpire($type, $data, $ttl);
  161. }
  162. }
  163. return $value;
  164. }
  165. return $key_exists ? $value : $default;
  166. }
  167. /**
  168. * Discovers the assets supported by the CDN Provider.
  169. *
  170. * CDN Providers should sub-class this method to make requests and/or process
  171. * any necessary data.
  172. *
  173. * @param string $version
  174. * The version of assets to return.
  175. * @param string $theme
  176. * A specific set of themed assets to return, if any.
  177. *
  178. * @return \Drupal\bootstrap\Plugin\Provider\CdnAssets
  179. * A CdnAssets object.
  180. */
  181. protected function discoverCdnAssets($version, $theme = NULL) {
  182. $assets = [];
  183. // Convert the deprecated array structure into a proper CdnAssets object.
  184. $data = $this->getAssets();
  185. foreach (['css', 'js'] as $type) {
  186. if (isset($data[$type])) {
  187. foreach ($data[$type] as $file) {
  188. $assets[] = new CdnAsset($file, NULL, $version);
  189. }
  190. }
  191. if (isset($data['min'][$type])) {
  192. foreach ($data['min'][$type] as $file) {
  193. $assets[] = new CdnAsset($file, NULL, $version);
  194. }
  195. }
  196. }
  197. return new CdnAssets($assets);
  198. }
  199. /**
  200. * Discovers the themes supported by the CDN Provider.
  201. *
  202. * CDN Providers should sub-class this method to make requests and/or process
  203. * any necessary data.
  204. *
  205. * @param string $version
  206. * A specific version of themes to retrieve.
  207. *
  208. * @return array|false
  209. * An associative array of theme data, similar to what is returned in
  210. * \Drupal\bootstrap\Plugin\Provider\ProviderBase::discoverCdnAssets(), but
  211. * keyed by the theme name.
  212. */
  213. protected function discoverCdnThemes($version) {
  214. return [];
  215. }
  216. /**
  217. * Discovers the versions supported by the CDN Provider.
  218. *
  219. * CDN Providers should sub-class this method to make requests and/or process
  220. * any necessary data.
  221. *
  222. * @return array|false
  223. * An associative array of versions, also keyed by the version.
  224. */
  225. protected function discoverCdnVersions() {
  226. return [];
  227. }
  228. /**
  229. * {@inheritdoc}
  230. */
  231. public function getCacheTtl($type) {
  232. if (!isset($this->cacheTtl[$type])) {
  233. $this->cacheTtl[$type] = (int) $this->theme->getSetting("cdn_cache_ttl_$type", static::TTL_NEVER);
  234. // If TTL is -1, the set a far reaching date from now.
  235. if ($this->cacheTtl[$type] === static::TTL_FOREVER) {
  236. $this->cacheTtl[$type] = static::TTL_ONE_YEAR * 10;
  237. }
  238. }
  239. return $this->cacheTtl[$type];
  240. }
  241. /**
  242. * Retrieves the unique cache identifier for the CDN Provider.
  243. *
  244. * @return string
  245. * The CDN Provider cache identifier.
  246. */
  247. protected function getCacheId() {
  248. return "theme:{$this->theme->getName()}:cdn:{$this->getPluginId()}";
  249. }
  250. /**
  251. * {@inheritdoc}
  252. */
  253. public function getCdnAssets($version = NULL, $theme = NULL) {
  254. if (!isset($this->cdnAssets)) {
  255. $this->cdnAssets = $this->cacheGet('assets');
  256. }
  257. $data = $this->getCdnAssetsCacheData($version, $theme);
  258. $hash = Crypt::generateBase64HashIdentifier($data);
  259. if (!isset($this->cdnAssets[$hash])) {
  260. $this->cdnAssets[$hash] = $this->cacheGet('assets', $hash, [], function () use ($data) {
  261. return $this->discoverCdnAssets($data['version'], $data['theme']);
  262. });
  263. }
  264. return $this->cdnAssets[$hash];
  265. }
  266. /**
  267. * Retrieves the data used to create a hash for CDN Assets.
  268. *
  269. * @param string $version
  270. * Optional. A specific version to use.
  271. * @param string $theme
  272. * Optional. A specific theme to use.
  273. *
  274. * @return array
  275. * An array of components that will be serialized and hashed.
  276. *
  277. * @see \Drupal\bootstrap\Plugin\Provider\ProviderBase::getCdnAssets()
  278. * @see \Drupal\bootstrap\Plugin\Provider\ProviderBase::alterFrameworkLibrary()
  279. */
  280. protected function getCdnAssetsCacheData($version = NULL, $theme = NULL) {
  281. if (!isset($version) && $this->supportsVersions()) {
  282. $version = $this->getCdnVersion();
  283. }
  284. if (!isset($theme) && $this->supportsThemes()) {
  285. $theme = $this->getCdnTheme();
  286. }
  287. return [
  288. 'ttl' => $this->getCacheTtl(static::CACHE_LIBRARY),
  289. 'min' => [
  290. 'css' => !!\Drupal::config('system.performance')->get('css.preprocess'),
  291. 'js' => !!\Drupal::config('system.performance')->get('js.preprocess'),
  292. ],
  293. 'provider' => $this->pluginId,
  294. 'version' => $version,
  295. 'theme' => $theme,
  296. ];
  297. }
  298. /**
  299. * {@inheritdoc}
  300. */
  301. public function getCdnExceptions($reset = TRUE) {
  302. $exceptions = $this->cdnExceptions;
  303. if ($reset) {
  304. $this->cdnExceptions = [];
  305. }
  306. return $exceptions;
  307. }
  308. /**
  309. * {@inheritdoc}
  310. */
  311. public function getCdnTheme() {
  312. return $this->supportsThemes() ? $this->theme->getSetting('cdn_theme', 'bootstrap') : NULL;
  313. }
  314. /**
  315. * {@inheritdoc}
  316. */
  317. public function getCdnThemes($version = NULL) {
  318. // Immediately return if the CDN Provider does not support themes.
  319. if (!$this->supportsThemes()) {
  320. return [];
  321. }
  322. $data = $this->getCdnThemesCacheData($version);
  323. $hash = Crypt::generateBase64HashIdentifier($data);
  324. if (!isset($this->themes[$hash])) {
  325. $this->themes[$hash] = $this->cacheGet('themes', $hash, [], function () use ($data) {
  326. return $this->discoverCdnThemes($data['version']);
  327. });
  328. }
  329. return $this->themes[$hash];
  330. }
  331. /**
  332. * Retrieves the data used to create a hash for CDN Themes.
  333. *
  334. * @param string $version
  335. * Optional. A specific version to use. If not set, the
  336. * currently set CDN version of the active theme will be used.
  337. *
  338. * @return array
  339. * An array of components that will be serialized and hashed.
  340. *
  341. * @see \Drupal\bootstrap\Plugin\Provider\ProviderBase::getCdnThemes()
  342. */
  343. protected function getCdnThemesCacheData($version = NULL) {
  344. if (!isset($version) && $this->supportsVersions()) {
  345. $version = $this->getCdnVersion();
  346. }
  347. return [
  348. 'ttl' => $this->getCacheTtl(static::CACHE_THEMES),
  349. 'provider' => $this->pluginId,
  350. 'version' => $version,
  351. ];
  352. }
  353. /**
  354. * {@inheritdoc}
  355. */
  356. public function getCdnVersion() {
  357. return $this->supportsVersions() ? $this->theme->getSetting('cdn_version', Bootstrap::FRAMEWORK_VERSION) : NULL;
  358. }
  359. /**
  360. * {@inheritdoc}
  361. */
  362. public function getCdnVersions() {
  363. // Immediately return if the CDN Provider does not support versions.
  364. if (!$this->supportsVersions()) {
  365. return [];
  366. }
  367. if (!isset($this->versions)) {
  368. $hash = Crypt::generateBase64HashIdentifier($this->getCdnVersionsCacheData());
  369. $this->versions = $this->cacheGet('versions', $hash, [], function () {
  370. return $this->discoverCdnVersions();
  371. });
  372. }
  373. return $this->versions;
  374. }
  375. /**
  376. * Retrieves the data used to create a hash for CDN Versions.
  377. *
  378. * @return array
  379. * An array of components that will be serialized and hashed.
  380. *
  381. * @see \Drupal\bootstrap\Plugin\Provider\ProviderBase::getCdnVersions()
  382. */
  383. protected function getCdnVersionsCacheData() {
  384. return [
  385. 'ttl' => $this->getCacheTtl(static::CACHE_THEMES),
  386. 'provider' => $this->pluginId,
  387. ];
  388. }
  389. /**
  390. * {@inheritdoc}
  391. */
  392. public function getDescription() {
  393. return $this->pluginDefinition['description'];
  394. }
  395. /**
  396. * Retrieves a permanent key/value storage instance.
  397. *
  398. * @return \Drupal\Core\KeyValueStore\KeyValueStoreInterface
  399. * A permanent key/value storage instance.
  400. */
  401. protected function getKeyValue() {
  402. if (!isset($this->keyValue)) {
  403. $this->keyValue = \Drupal::keyValue($this->getCacheId());
  404. }
  405. return $this->keyValue;
  406. }
  407. /**
  408. * Retrieves a expirable key/value storage instance.
  409. *
  410. * @return \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface
  411. * An expirable key/value storage instance.
  412. */
  413. protected function getKeyValueExpirable() {
  414. if (!isset($this->keyValueExpirable)) {
  415. $this->keyValueExpirable = \Drupal::keyValueExpirable($this->getCacheId());
  416. }
  417. return $this->keyValueExpirable;
  418. }
  419. /**
  420. * {@inheritdoc}
  421. */
  422. public function getLabel() {
  423. return $this->pluginDefinition['label'] ?: $this->getPluginId();
  424. }
  425. /**
  426. * {@inheritdoc}
  427. */
  428. public function getThemes() {
  429. return $this->pluginDefinition['themes'];
  430. }
  431. /**
  432. * {@inheritdoc}
  433. */
  434. public function getVersions() {
  435. return $this->pluginDefinition['versions'];
  436. }
  437. /**
  438. * Allows providers a way to map a version to a different version.
  439. *
  440. * @param string $version
  441. * The version to map.
  442. *
  443. * @return string
  444. * The mapped version.
  445. */
  446. protected function mapVersion($version) {
  447. return $version;
  448. }
  449. /**
  450. * Initiates an HTTP request.
  451. *
  452. * @param string $url
  453. * The URL to retrieve.
  454. * @param array $options
  455. * The options to pass to the HTTP client.
  456. *
  457. * @return \Drupal\bootstrap\SerializedResponse
  458. * A SerializedResponse object.
  459. */
  460. protected function request($url, array $options = []) {
  461. $response = Bootstrap::request($url, $options, $exception);
  462. if ($exception) {
  463. $this->addCdnException($exception);
  464. }
  465. return $response;
  466. }
  467. /**
  468. * {@inheritdoc}
  469. */
  470. public function resetCache() {
  471. $this->getKeyValue()->deleteAll();
  472. $this->getKeyValueExpirable()->deleteAll();
  473. // Invalidate library info if this provider is the one currently used.
  474. if ($this->theme->getCdnProvider()->getPluginId() === $this->pluginId) {
  475. /** @var \Drupal\Core\Cache\CacheTagsInvalidatorInterface $invalidator */
  476. $invalidator = \Drupal::service('cache_tags.invalidator');
  477. $invalidator->invalidateTags(['library_info']);
  478. }
  479. }
  480. /**
  481. * Sets CDN Provider exceptions, replacing any existing exceptions.
  482. *
  483. * @param \Throwable[] $exceptions
  484. * The Exceptions to set.
  485. *
  486. * @return static
  487. */
  488. protected function setCdnExceptions(array $exceptions) {
  489. $this->cdnExceptions = [];
  490. foreach ($exceptions as $exception) {
  491. $this->addCdnException($exception);
  492. }
  493. return $this;
  494. }
  495. /**
  496. * {@inheritdoc}
  497. */
  498. public function supportsThemes() {
  499. return TRUE;
  500. }
  501. /**
  502. * {@inheritdoc}
  503. */
  504. public function supportsVersions() {
  505. return TRUE;
  506. }
  507. /**
  508. * {@inheritdoc}
  509. */
  510. public function trackCdnExceptions(callable $callable) {
  511. // Retrieve existing exceptions.
  512. $existing = $this->getCdnExceptions();
  513. // Execute the callable.
  514. $callable($this);
  515. // Retrieve any newly generated exceptions.
  516. $new = $this->getCdnExceptions();
  517. // Merge the existing and newly generated exceptions and set them.
  518. $this->setCdnExceptions(array_merge($existing, $new));
  519. // Return the newly generated exceptions.
  520. return $new;
  521. }
  522. /****************************************************************************
  523. *
  524. * Deprecated methods
  525. *
  526. ***************************************************************************/
  527. /**
  528. * {@inheritdoc}
  529. *
  530. * @deprecated in 8.x-3.18, will be removed in a future release.
  531. */
  532. public function getApi() {
  533. Bootstrap::deprecated();
  534. return $this->pluginDefinition['api'];
  535. }
  536. /**
  537. * {@inheritdoc}
  538. *
  539. * @deprecated in 8.x-3.18, will be removed in a future release.
  540. */
  541. public function getAssets($types = NULL) {
  542. Bootstrap::deprecated();
  543. return $this->assets;
  544. }
  545. /**
  546. * {@inheritdoc}
  547. *
  548. * @deprecated in 8.x-3.18, will be removed in a future release.
  549. */
  550. public function hasError() {
  551. Bootstrap::deprecated();
  552. return $this->pluginDefinition['error'];
  553. }
  554. /**
  555. * {@inheritdoc}
  556. *
  557. * @deprecated in 8.x-3.18, will be removed in a future release.
  558. */
  559. public function isImported() {
  560. Bootstrap::deprecated();
  561. return $this->pluginDefinition['imported'];
  562. }
  563. /**
  564. * {@inheritdoc}
  565. *
  566. * @deprecated in 8.x-3.18, will be removed in a future release.
  567. */
  568. public function processDefinition(array &$definition, $plugin_id) {
  569. // Due to code recursion and the need to keep this code in place for BC
  570. // reasons, this deprecated message should only be logged and not shown.
  571. Bootstrap::deprecated(FALSE);
  572. // Process API data.
  573. if ($api = $this->getApi()) {
  574. $provider_path = ProviderManager::FILE_PATH;
  575. file_prepare_directory($provider_path, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
  576. // Use manually imported API data, if it exists.
  577. if (file_exists("$provider_path/$plugin_id.json") && ($imported_data = file_get_contents("$provider_path/$plugin_id.json"))) {
  578. $definition['imported'] = TRUE;
  579. try {
  580. $json = Json::decode($imported_data);
  581. }
  582. catch (\Exception $e) {
  583. // Intentionally left blank.
  584. }
  585. }
  586. // Otherwise, attempt to request API data if the provider has specified
  587. // an "api" URL to use.
  588. else {
  589. $json = Bootstrap::request($api)->getData();
  590. }
  591. if (!isset($json)) {
  592. $json = [];
  593. $definition['error'] = TRUE;
  594. }
  595. $this->processApi($json, $definition);
  596. }
  597. }
  598. /**
  599. * {@inheritdoc}
  600. *
  601. * @deprecated in 8.x-3.18, will be removed in a future release.
  602. */
  603. public function processApi(array $json, array &$definition) {
  604. Bootstrap::deprecated();
  605. }
  606. }

Classes

Name Description
ProviderBase CDN Provider base class.