[ Index ]

PHP Cross Reference of Joomla 4.2.2 documentation

title

Body

[close]

/libraries/src/WebAsset/ -> WebAssetManager.php (source)

   1  <?php
   2  
   3  /**
   4   * Joomla! Content Management System
   5   *
   6   * @copyright  (C) 2019 Open Source Matters, Inc. <https://www.joomla.org>
   7   * @license    GNU General Public License version 2 or later; see LICENSE.txt
   8   */
   9  
  10  namespace Joomla\CMS\WebAsset;
  11  
  12  use Joomla\CMS\Event\WebAsset\WebAssetRegistryAssetChanged;
  13  use Joomla\CMS\WebAsset\Exception\InvalidActionException;
  14  use Joomla\CMS\WebAsset\Exception\UnknownAssetException;
  15  use Joomla\CMS\WebAsset\Exception\UnsatisfiedDependencyException;
  16  
  17  // phpcs:disable PSR1.Files.SideEffects
  18  \defined('JPATH_PLATFORM') or die;
  19  // phpcs:enable PSR1.Files.SideEffects
  20  
  21  /**
  22   * Web Asset Manager class
  23   *
  24   * @method WebAssetManager registerStyle(WebAssetItem|string $asset, string $uri = '', $options = [], $attributes = [], $dependencies = [])
  25   * @method WebAssetManager registerAndUseStyle(WebAssetItem|string $asset, string $uri = '', $options = [], $attributes = [], $dependencies = [])
  26   * @method WebAssetManager useStyle($name)
  27   * @method WebAssetManager disableStyle($name)
  28   * @method WebAssetManager addInlineStyle(WebAssetItem|string $content, $options = [], $attributes = [], $dependencies = [])
  29   *
  30   * @method WebAssetManager registerScript(WebAssetItem|string $asset, string $uri = '', $options = [], $attributes = [], $dependencies = [])
  31   * @method WebAssetManager registerAndUseScript(WebAssetItem|string $asset, string $uri = '', $options = [], $attributes = [], $dependencies = [])
  32   * @method WebAssetManager useScript($name)
  33   * @method WebAssetManager disableScript($name)
  34   * @method WebAssetManager addInlineScript(WebAssetItem|string $content, $options = [], $attributes = [], $dependencies = [])
  35   *
  36   * @method WebAssetManager registerPreset(WebAssetItem|string $asset, string $uri = '', $options = [], $attributes = [], $dependencies = [])
  37   * @method WebAssetManager registerAndUsePreset(WebAssetItem|string $asset, string $uri = '', $options = [], $attributes = [], $dependencies = [])
  38   * @method WebAssetManager usePreset($name)
  39   * @method WebAssetManager disablePreset($name)
  40   *
  41   * @since  4.0.0
  42   */
  43  class WebAssetManager implements WebAssetManagerInterface
  44  {
  45      /**
  46       * Mark inactive asset
  47       *
  48       * @var    integer
  49       *
  50       * @since  4.0.0
  51       */
  52      public const ASSET_STATE_INACTIVE = 0;
  53  
  54      /**
  55       * Mark active asset. Just enabled, but WITHOUT dependency resolved
  56       *
  57       * @var    integer
  58       *
  59       * @since  4.0.0
  60       */
  61      public const ASSET_STATE_ACTIVE = 1;
  62  
  63      /**
  64       * Mark active asset that is enabled as dependency to another asset
  65       *
  66       * @var    integer
  67       *
  68       * @since  4.0.0
  69       */
  70      public const ASSET_STATE_DEPENDENCY = 2;
  71  
  72      /**
  73       * The WebAsset Registry instance
  74       *
  75       * @var    WebAssetRegistry
  76       *
  77       * @since  4.0.0
  78       */
  79      protected $registry;
  80  
  81      /**
  82       * A list of active assets (including their dependencies).
  83       * Array of Name => State
  84       *
  85       * @var    array
  86       *
  87       * @since  4.0.0
  88       */
  89      protected $activeAssets = [];
  90  
  91      /**
  92       * Internal marker to check the manager state,
  93       * to prevent use of the manager after an assets are rendered
  94       *
  95       * @var    boolean
  96       *
  97       * @since  4.0.0
  98       */
  99      protected $locked = false;
 100  
 101      /**
 102       * Internal marker to keep track when need to recheck dependencies
 103       *
 104       * @var    boolean
 105       *
 106       * @since  4.0.0
 107       */
 108      protected $dependenciesIsActual = false;
 109  
 110      /**
 111       * Class constructor
 112       *
 113       * @param   WebAssetRegistry  $registry  The WebAsset Registry instance
 114       *
 115       * @since   4.0.0
 116       */
 117      public function __construct(WebAssetRegistry $registry)
 118      {
 119          $this->registry = $registry;
 120  
 121          // Listen to changes in the registry
 122          $this->registry->getDispatcher()->addListener(
 123              'onWebAssetRegistryChangedAssetOverride',
 124              function (WebAssetRegistryAssetChanged $event) {
 125                  // If the changed asset are used
 126                  if (!empty($this->activeAssets[$event->getAssetType()][$event->getAsset()->getName()])) {
 127                      $this->dependenciesIsActual = false;
 128                  }
 129              }
 130          );
 131  
 132          $this->registry->getDispatcher()->addListener(
 133              'onWebAssetRegistryChangedAssetRemove',
 134              function (WebAssetRegistryAssetChanged $event) {
 135                  // If the changed asset are used
 136                  if (!empty($this->activeAssets[$event->getAssetType()][$event->getAsset()->getName()])) {
 137                      $this->dependenciesIsActual = false;
 138                  }
 139              }
 140          );
 141      }
 142  
 143      /**
 144       * Get associated registry instance
 145       *
 146       * @return   WebAssetRegistry
 147       *
 148       * @since  4.0.0
 149       */
 150      public function getRegistry(): WebAssetRegistry
 151      {
 152          return $this->registry;
 153      }
 154  
 155      /**
 156       * Clears all collected items.
 157       *
 158       * @return self
 159       *
 160       * @since  4.1.1
 161       */
 162      public function reset(): WebAssetManagerInterface
 163      {
 164          if ($this->locked) {
 165              throw new InvalidActionException('WebAssetManager is locked');
 166          }
 167  
 168          $this->activeAssets = [];
 169          $this->dependenciesIsActual = false;
 170  
 171          return $this;
 172      }
 173  
 174      /**
 175       * Adds support for magic method calls
 176       *
 177       * @param   string  $method     A method name
 178       * @param   array   $arguments  Arguments for a method
 179       *
 180       * @return mixed
 181       *
 182       * @throws  \BadMethodCallException
 183       *
 184       * @since  4.0.0
 185       */
 186      public function __call($method, $arguments)
 187      {
 188          $method = strtolower($method);
 189  
 190          if (0 === strpos($method, 'use')) {
 191              $type = substr($method, 3);
 192  
 193              if (empty($arguments[0])) {
 194                  throw new \BadMethodCallException('An asset name is required');
 195              }
 196  
 197              return $this->useAsset($type, $arguments[0]);
 198          }
 199  
 200          if (0 === strpos($method, 'addinline')) {
 201              $type = substr($method, 9);
 202  
 203              if (empty($arguments[0])) {
 204                  throw new \BadMethodCallException('Content is required');
 205              }
 206  
 207              return $this->addInline($type, ...$arguments);
 208          }
 209  
 210          if (0 === strpos($method, 'disable')) {
 211              $type = substr($method, 7);
 212  
 213              if (empty($arguments[0])) {
 214                  throw new \BadMethodCallException('An asset name is required');
 215              }
 216  
 217              return $this->disableAsset($type, $arguments[0]);
 218          }
 219  
 220          if (0 === strpos($method, 'register')) {
 221              // Check for registerAndUse<Type>
 222              $andUse = substr($method, 8, 6) === 'anduse';
 223  
 224              // Extract the type
 225              $type = $andUse ? substr($method, 14) : substr($method, 8);
 226  
 227              if (empty($arguments[0])) {
 228                  throw new \BadMethodCallException('An asset instance or an asset name is required');
 229              }
 230  
 231              if ($andUse) {
 232                  $name = $arguments[0] instanceof WebAssetItemInterface ? $arguments[0]->getName() : $arguments[0];
 233  
 234                  return $this->registerAsset($type, ...$arguments)->useAsset($type, $name);
 235              } else {
 236                  return $this->registerAsset($type, ...$arguments);
 237              }
 238          }
 239  
 240          throw new \BadMethodCallException(sprintf('Undefined method %s in class %s', $method, get_class($this)));
 241      }
 242  
 243      /**
 244       * Enable an asset item to be attached to a Document
 245       *
 246       * @param   string  $type  The asset type, script or style
 247       * @param   string  $name  The asset name
 248       *
 249       * @return self
 250       *
 251       * @throws  UnknownAssetException  When Asset cannot be found
 252       * @throws  InvalidActionException When the Manager already attached to a Document
 253       *
 254       * @since  4.0.0
 255       */
 256      public function useAsset(string $type, string $name): WebAssetManagerInterface
 257      {
 258          if ($this->locked) {
 259              throw new InvalidActionException('WebAssetManager is locked, you came late');
 260          }
 261  
 262          // Check whether asset exists
 263          $asset = $this->registry->get($type, $name);
 264  
 265          if (empty($this->activeAssets[$type])) {
 266              $this->activeAssets[$type] = [];
 267          }
 268  
 269          // For "preset" need to check the dependencies first
 270          if ($type === 'preset') {
 271              $this->usePresetItems($name);
 272          }
 273  
 274          // Asset already enabled
 275          if (!empty($this->activeAssets[$type][$name])) {
 276              // Set state to active, in case it was ASSET_STATE_DEPENDENCY
 277              $this->activeAssets[$type][$name] = static::ASSET_STATE_ACTIVE;
 278  
 279              return $this;
 280          }
 281  
 282          $this->activeAssets[$type][$name] = static::ASSET_STATE_ACTIVE;
 283  
 284          // To re-check dependencies
 285          if ($asset->getDependencies()) {
 286              $this->dependenciesIsActual = false;
 287          }
 288  
 289          return $this;
 290      }
 291  
 292      /**
 293       * Deactivate an asset item, so it will not be attached to a Document
 294       *
 295       * @param   string  $type  The asset type, script or style
 296       * @param   string  $name  The asset name
 297       *
 298       * @return  self
 299       *
 300       * @throws  UnknownAssetException  When Asset cannot be found
 301       * @throws  InvalidActionException When the Manager already attached to a Document
 302       *
 303       * @since  4.0.0
 304       */
 305      public function disableAsset(string $type, string $name): WebAssetManagerInterface
 306      {
 307          if ($this->locked) {
 308              throw new InvalidActionException('WebAssetManager is locked, you came late');
 309          }
 310  
 311          // Check whether asset exists
 312          $this->registry->get($type, $name);
 313  
 314          unset($this->activeAssets[$type][$name]);
 315  
 316          // To re-check dependencies
 317          $this->dependenciesIsActual = false;
 318  
 319          // For Preset case
 320          if ($type === 'preset') {
 321              $this->disablePresetItems($name);
 322          }
 323  
 324          return $this;
 325      }
 326  
 327      /**
 328       * Enable list of assets provided by Preset item.
 329       *
 330       * "Preset" a special kind of asset that hold a list of assets that has to be enabled,
 331       * same as direct call of useAsset() to each of item in list.
 332       * Can hold mixed types of assets (script, style, another preset, etc), the type provided after # symbol, after
 333       * the asset name, example: foo#style, bar#script.
 334       *
 335       * The method call useAsset() internally for each of its dependency, this is important for keeping FIFO order
 336       * of enabled items.
 337       * The Preset not a strict asset, and each of its dependency can be safely disabled by use of disableAsset() later.
 338       *
 339       * @param   string  $name  The asset name
 340       *
 341       * @return self
 342       *
 343       * @throws  UnsatisfiedDependencyException  When Asset dependency cannot be found
 344       *
 345       * @since  4.0.0
 346       */
 347      protected function usePresetItems($name): WebAssetManagerInterface
 348      {
 349          // Get the asset object
 350          $asset = $this->registry->get('preset', $name);
 351  
 352          // Call useAsset() to each of its dependency
 353          foreach ($asset->getDependencies() as $dependency) {
 354              $depType = '';
 355              $depName = $dependency;
 356              $pos     = strrpos($dependency, '#');
 357  
 358              // Check for cross-dependency "dependency-name#type" case
 359              if ($pos) {
 360                  $depType = substr($dependency, $pos + 1);
 361                  $depName = substr($dependency, 0, $pos);
 362              }
 363  
 364              $depType = $depType ?: 'preset';
 365  
 366              // Make sure dependency exists
 367              if (!$this->registry->exists($depType, $depName)) {
 368                  throw new UnsatisfiedDependencyException(
 369                      sprintf('Unsatisfied dependency "%s" for an asset "%s" of type "%s"', $dependency, $name, 'preset')
 370                  );
 371              }
 372  
 373              $this->useAsset($depType, $depName);
 374          }
 375  
 376          return $this;
 377      }
 378  
 379      /**
 380       * Deactivate list of assets provided by Preset item.
 381       *
 382       * @param   string  $name  The asset name
 383       *
 384       * @return  self
 385       *
 386       * @throws  UnsatisfiedDependencyException  When Asset dependency cannot be found
 387       *
 388       * @since  4.0.0
 389       */
 390      protected function disablePresetItems($name): WebAssetManagerInterface
 391      {
 392          // Get the asset object
 393          $asset = $this->registry->get('preset', $name);
 394  
 395          // Call disableAsset() to each of its dependency
 396          foreach ($asset->getDependencies() as $dependency) {
 397              $depType = '';
 398              $depName = $dependency;
 399              $pos     = strrpos($dependency, '#');
 400  
 401              // Check for cross-dependency "dependency-name#type" case
 402              if ($pos) {
 403                  $depType = substr($dependency, $pos + 1);
 404                  $depName = substr($dependency, 0, $pos);
 405              }
 406  
 407              $depType = $depType ?: 'preset';
 408  
 409              // Make sure dependency exists
 410              if (!$this->registry->exists($depType, $depName)) {
 411                  throw new UnsatisfiedDependencyException(
 412                      sprintf('Unsatisfied dependency "%s" for an asset "%s" of type "%s"', $dependency, $name, 'preset')
 413                  );
 414              }
 415  
 416              $this->disableAsset($depType, $depName);
 417          }
 418  
 419          return $this;
 420      }
 421  
 422      /**
 423       * Get a state for the Asset
 424       *
 425       * @param   string  $type  The asset type, script or style
 426       * @param   string  $name  The asset name
 427       *
 428       * @return  integer
 429       *
 430       * @throws  UnknownAssetException  When Asset cannot be found
 431       *
 432       * @since  4.0.0
 433       */
 434      public function getAssetState(string $type, string $name): int
 435      {
 436          // Check whether asset exists first
 437          $this->registry->get($type, $name);
 438  
 439          // Make sure that all dependencies are active
 440          if (!$this->dependenciesIsActual) {
 441              $this->enableDependencies();
 442          }
 443  
 444          if (!empty($this->activeAssets[$type][$name])) {
 445              return $this->activeAssets[$type][$name];
 446          }
 447  
 448          return static::ASSET_STATE_INACTIVE;
 449      }
 450  
 451      /**
 452       * Check whether the asset are enabled
 453       *
 454       * @param   string  $type  The asset type, script or style
 455       * @param   string  $name  The asset name
 456       *
 457       * @return  boolean
 458       *
 459       * @throws  UnknownAssetException  When Asset cannot be found
 460       *
 461       * @since  4.0.0
 462       */
 463      public function isAssetActive(string $type, string $name): bool
 464      {
 465          return $this->getAssetState($type, $name) !== static::ASSET_STATE_INACTIVE;
 466      }
 467  
 468      /**
 469       * Helper method to check whether the asset exists in the registry.
 470       *
 471       * @param   string  $type  Asset type, script or style
 472       * @param   string  $name  Asset name
 473       *
 474       * @return  boolean
 475       *
 476       * @since   4.0.0
 477       */
 478      public function assetExists(string $type, string $name): bool
 479      {
 480          return $this->registry->exists($type, $name);
 481      }
 482  
 483      /**
 484       * Register a new asset.
 485       * Allow to register WebAssetItem instance in the registry, by call registerAsset($type, $assetInstance)
 486       * Or create an asset on fly (from name and Uri) and register in the registry, by call registerAsset($type, $assetName, $uri, $options ....)
 487       *
 488       * @param   string               $type          The asset type, script or style
 489       * @param   WebAssetItem|string  $asset         The asset name or instance to register
 490       * @param   string               $uri           The URI for the asset
 491       * @param   array                $options       Additional options for the asset
 492       * @param   array                $attributes    Attributes for the asset
 493       * @param   array                $dependencies  Asset dependencies
 494       *
 495       * @return  self
 496       *
 497       * @since  4.0.0
 498       *
 499       * @throws  \InvalidArgumentException
 500       */
 501      public function registerAsset(string $type, $asset, string $uri = '', array $options = [], array $attributes = [], array $dependencies = [])
 502      {
 503          if ($asset instanceof WebAssetItemInterface) {
 504              $this->registry->add($type, $asset);
 505          } elseif (is_string($asset)) {
 506              $options['type'] = $type;
 507              $assetInstance = $this->registry->createAsset($asset, $uri, $options, $attributes, $dependencies);
 508              $this->registry->add($type, $assetInstance);
 509          } else {
 510              throw new \InvalidArgumentException(
 511                  sprintf(
 512                      '%s(): Argument #2 ($asset) must be a string or an instance of %s, %s given.',
 513                      __METHOD__,
 514                      WebAssetItemInterface::class,
 515                      \is_object($asset) ? \get_class($asset) : \gettype($asset)
 516                  )
 517              );
 518          }
 519  
 520          return $this;
 521      }
 522  
 523      /**
 524       * Helper method to get the asset from the registry.
 525       *
 526       * @param   string  $type  Asset type, script or style
 527       * @param   string  $name  Asset name
 528       *
 529       * @return  WebAssetItemInterface
 530       *
 531       * @throws  UnknownAssetException  When Asset cannot be found
 532       *
 533       * @since   4.0.0
 534       */
 535      public function getAsset(string $type, string $name): WebAssetItemInterface
 536      {
 537          return $this->registry->get($type, $name);
 538      }
 539  
 540      /**
 541       * Get all active assets, optionally sort them to follow the dependency Graph
 542       *
 543       * @param   string  $type  The asset type, script or style
 544       * @param   bool    $sort  Whether need to sort the assets to follow the dependency Graph
 545       *
 546       * @return  WebAssetItem[]
 547       *
 548       * @throws  UnknownAssetException  When Asset cannot be found
 549       * @throws  UnsatisfiedDependencyException When Dependency cannot be found
 550       *
 551       * @since  4.0.0
 552       */
 553      public function getAssets(string $type, bool $sort = false): array
 554      {
 555          // Make sure that all dependencies are active
 556          if (!$this->dependenciesIsActual) {
 557              $this->enableDependencies();
 558          }
 559  
 560          if (empty($this->activeAssets[$type])) {
 561              return [];
 562          }
 563  
 564          // Apply Tree sorting for regular asset items, but return FIFO order for "preset"
 565          if ($sort && $type !== 'preset') {
 566              $assets = $this->calculateOrderOfActiveAssets($type);
 567          } else {
 568              $assets = [];
 569  
 570              foreach (array_keys($this->activeAssets[$type]) as $name) {
 571                  $assets[$name] = $this->registry->get($type, $name);
 572              }
 573          }
 574  
 575          return $assets;
 576      }
 577  
 578      /**
 579       * Helper method to calculate inline to non inline relation (before/after positions).
 580       * Return associated array, which contain dependency (handle) name as key, and list of inline items for each position.
 581       * Example: ['handle.name' => ['before' => ['inline1', 'inline2'], 'after' => ['inline3', 'inline4']]]
 582       *
 583       * Note: If inline asset have a multiple dependencies, then will be used last one from the list for positioning
 584       *
 585       * @param   WebAssetItem[]  $assets  The assets list
 586       *
 587       * @return  array
 588       *
 589       * @since  4.0.0
 590       */
 591      public function getInlineRelation(array $assets): array
 592      {
 593          $inlineRelation = [];
 594  
 595          // Find an inline assets and their relations to non inline
 596          foreach ($assets as $k => $asset) {
 597              if (!$asset->getOption('inline')) {
 598                  continue;
 599              }
 600  
 601              // Add to list of inline assets
 602              $inlineAssets[$asset->getName()] = $asset;
 603  
 604              // Check whether position are requested with dependencies
 605              $position = $asset->getOption('position');
 606              $position = $position === 'before' || $position === 'after' ? $position : null;
 607              $deps     = $asset->getDependencies();
 608  
 609              if ($position && $deps) {
 610                  // If inline asset have a multiple dependencies, then use last one from the list for positioning
 611                  $handle = end($deps);
 612                  $inlineRelation[$handle][$position][$asset->getName()] = $asset;
 613              }
 614          }
 615  
 616          return $inlineRelation;
 617      }
 618  
 619      /**
 620       * Helper method to filter an inline assets
 621       *
 622       * @param   WebAssetItem[]  $assets  Reference to a full list of active assets
 623       *
 624       * @return  WebAssetItem[]  Array of inline assets
 625       *
 626       * @since  4.0.0
 627       */
 628      public function filterOutInlineAssets(array &$assets): array
 629      {
 630          $inlineAssets = [];
 631  
 632          foreach ($assets as $k => $asset) {
 633              if (!$asset->getOption('inline')) {
 634                  continue;
 635              }
 636  
 637              // Remove inline assets from assets list, and add to list of inline
 638              unset($assets[$k]);
 639  
 640              $inlineAssets[$asset->getName()] = $asset;
 641          }
 642  
 643          return $inlineAssets;
 644      }
 645  
 646      /**
 647       * Add a new inline content asset.
 648       * Allow to register WebAssetItem instance in the registry, by call addInline($type, $assetInstance)
 649       * Or create an asset on fly (from name and Uri) and register in the registry, by call addInline($type, $content, $options ....)
 650       *
 651       * @param   string               $type          The asset type, script or style
 652       * @param   WebAssetItem|string  $content       The content to of inline asset
 653       * @param   array                $options       Additional options for the asset
 654       * @param   array                $attributes    Attributes for the asset
 655       * @param   array                $dependencies  Asset dependencies
 656       *
 657       * @return  self
 658       *
 659       * @since  4.0.0
 660       *
 661       * @throws \InvalidArgumentException
 662       */
 663      public function addInline(string $type, $content, array $options = [], array $attributes = [], array $dependencies = []): self
 664      {
 665          if ($content instanceof WebAssetItemInterface) {
 666              $assetInstance = $content;
 667          } elseif (is_string($content)) {
 668              $name          = $options['name'] ?? ('inline.' . md5($content));
 669              $assetInstance = $this->registry->createAsset($name, '', $options, $attributes, $dependencies);
 670              $assetInstance->setOption('content', $content);
 671          } else {
 672              throw new \InvalidArgumentException(
 673                  sprintf(
 674                      '%s(): Argument #2 ($content) must be a string or an instance of %s, %s given.',
 675                      __METHOD__,
 676                      WebAssetItemInterface::class,
 677                      \is_object($content) ? \get_class($content) : \gettype($content)
 678                  )
 679              );
 680          }
 681  
 682          // Get the name
 683          $asset = $assetInstance->getName();
 684  
 685          // Set required options
 686          $assetInstance->setOption('type', $type);
 687          $assetInstance->setOption('inline', true);
 688  
 689          // Add to registry
 690          $this->registry->add($type, $assetInstance);
 691  
 692          // And make active
 693          $this->useAsset($type, $asset);
 694  
 695          return $this;
 696      }
 697  
 698      /**
 699       * Lock the manager to prevent further modifications
 700       *
 701       * @return self
 702       *
 703       * @since  4.0.0
 704       */
 705      public function lock(): self
 706      {
 707          $this->locked = true;
 708  
 709          return $this;
 710      }
 711  
 712      /**
 713       * Get the manager state. A collection of registry files and active asset names (per type).
 714       *
 715       * @return array
 716       *
 717       * @since  4.0.0
 718       */
 719      public function getManagerState(): array
 720      {
 721          return [
 722              'registryFiles' => $this->getRegistry()->getRegistryFiles(),
 723              'activeAssets'  => $this->activeAssets,
 724          ];
 725      }
 726  
 727      /**
 728       * Update Dependencies state for all active Assets or only for given
 729       *
 730       * @param   string        $type   The asset type, script or style
 731       * @param   WebAssetItem  $asset  The asset instance to which need to enable dependencies
 732       *
 733       * @return  self
 734       *
 735       * @since  4.0.0
 736       */
 737      protected function enableDependencies(string $type = null, WebAssetItem $asset = null): self
 738      {
 739          if ($type === 'preset') {
 740              // Preset items already was enabled by usePresetItems()
 741              return $this;
 742          }
 743  
 744          if ($asset) {
 745              // Get all dependencies of given asset recursively
 746              $allDependencies = $this->getDependenciesForAsset($type, $asset, true);
 747  
 748              foreach ($allDependencies as $depType => $depItems) {
 749                  foreach ($depItems as $depItem) {
 750                      // Set dependency state only when it is inactive, to keep a manually activated Asset in their original state
 751                      if (empty($this->activeAssets[$depType][$depItem->getName()])) {
 752                          // Add the dependency at the top of the list of active assets
 753                          $this->activeAssets[$depType] = [$depItem->getName() => static::ASSET_STATE_DEPENDENCY] + $this->activeAssets[$depType];
 754                      }
 755                  }
 756              }
 757          } else {
 758              // Re-Check for dependencies for all active assets
 759              // Firstly, filter out only active assets
 760              foreach ($this->activeAssets as $type => $activeAsset) {
 761                  $this->activeAssets[$type] = array_filter(
 762                      $activeAsset,
 763                      function ($state) {
 764                          return $state === WebAssetManager::ASSET_STATE_ACTIVE;
 765                      }
 766                  );
 767              }
 768  
 769              // Secondary, check for dependencies of each active asset
 770              // This need to be separated from previous step because we may have "cross type" dependency
 771              foreach ($this->activeAssets as $type => $activeAsset) {
 772                  foreach (array_keys($activeAsset) as $name) {
 773                      $asset = $this->registry->get($type, $name);
 774                      $this->enableDependencies($type, $asset);
 775                  }
 776              }
 777  
 778              $this->dependenciesIsActual = true;
 779          }
 780  
 781          return $this;
 782      }
 783  
 784      /**
 785       * Calculate weight of active Assets, by its Dependencies
 786       *
 787       * @param   string  $type  The asset type, script or style
 788       *
 789       * @return  WebAssetItem[]
 790       *
 791       * @since  4.0.0
 792       */
 793      protected function calculateOrderOfActiveAssets($type): array
 794      {
 795          // See https://en.wikipedia.org/wiki/Topological_sorting#Kahn.27s_algorithm
 796          $graphOrder    = [];
 797          $activeAssets  = $this->getAssets($type, false);
 798  
 799          // Get Graph of Outgoing and Incoming connections
 800          $connectionsGraph = $this->getConnectionsGraph($activeAssets);
 801          $graphOutgoing    = $connectionsGraph['outgoing'];
 802          $graphIncoming    = $connectionsGraph['incoming'];
 803  
 804          // Make a copy to be used during weight processing
 805          $graphIncomingCopy = $graphIncoming;
 806  
 807          // Find items without incoming connections
 808          $emptyIncoming = array_keys(
 809              array_filter(
 810                  $graphIncoming,
 811                  function ($el) {
 812                      return !$el;
 813                  }
 814              )
 815          );
 816  
 817          // Reverse, to start from a last enabled and move up to a first enabled, this helps to maintain an original sorting
 818          $emptyIncoming = array_reverse($emptyIncoming);
 819  
 820          // Loop through, and sort the graph
 821          while ($emptyIncoming) {
 822              // Add the node without incoming connection to the result
 823              $item = array_shift($emptyIncoming);
 824              $graphOrder[] = $item;
 825  
 826              // Check of each neighbor of the node
 827              foreach (array_reverse($graphOutgoing[$item]) as $neighbor) {
 828                  // Remove incoming connection of already visited node
 829                  unset($graphIncoming[$neighbor][$item]);
 830  
 831                  // If there no more incoming connections add the node to queue
 832                  if (empty($graphIncoming[$neighbor])) {
 833                      $emptyIncoming[] = $neighbor;
 834                  }
 835              }
 836          }
 837  
 838          // Sync Graph order with FIFO order
 839          $fifoWeights      = [];
 840          $graphWeights     = [];
 841          $requestedWeights = [];
 842  
 843          foreach (array_keys($this->activeAssets[$type]) as $index => $name) {
 844              $fifoWeights[$name] = $index * 10 + 10;
 845          }
 846  
 847          foreach (array_reverse($graphOrder) as $index => $name) {
 848              $graphWeights[$name]     = $index * 10 + 10;
 849              $requestedWeights[$name] = $activeAssets[$name]->getOption('weight') ?: $fifoWeights[$name];
 850          }
 851  
 852          // Try to set a requested weight, or make it close as possible to requested, but keep the Graph order
 853          while ($requestedWeights) {
 854              $item   = key($requestedWeights);
 855              $weight = array_shift($requestedWeights);
 856  
 857              // Skip empty items
 858              if ($weight === null) {
 859                  continue;
 860              }
 861  
 862              // Check the predecessors (Outgoing vertexes), the weight cannot be lighter than the predecessor have
 863              $topBorder = $weight - 1;
 864  
 865              if (!empty($graphOutgoing[$item])) {
 866                  $prevWeights = [];
 867  
 868                  foreach ($graphOutgoing[$item] as $pItem) {
 869                      $prevWeights[] = $graphWeights[$pItem];
 870                  }
 871  
 872                  $topBorder = max($prevWeights);
 873              }
 874  
 875              // Calculate a new weight
 876              $newWeight = $weight > $topBorder ? $weight : $topBorder + 1;
 877  
 878              // If a new weight heavier than existing, then we need to update all incoming connections (children)
 879              if ($newWeight > $graphWeights[$item] && !empty($graphIncomingCopy[$item])) {
 880                  // Sort Graph of incoming by actual position
 881                  foreach ($graphIncomingCopy[$item] as $incomingItem) {
 882                      // Set a weight heavier than current, then this node to be processed in next iteration
 883                      if (empty($requestedWeights[$incomingItem])) {
 884                          $requestedWeights[$incomingItem] = $graphWeights[$incomingItem] + $newWeight;
 885                      }
 886                  }
 887              }
 888  
 889              // Set a new weight
 890              $graphWeights[$item] = $newWeight;
 891          }
 892  
 893          asort($graphWeights);
 894  
 895          // Get Assets in calculated order
 896          $resultAssets  = [];
 897  
 898          foreach (array_keys($graphWeights) as $name) {
 899              $resultAssets[$name] = $activeAssets[$name];
 900          }
 901  
 902          return $resultAssets;
 903      }
 904  
 905      /**
 906       * Build Graph of Outgoing and Incoming connections for given assets.
 907       *
 908       * @param   WebAssetItem[]  $assets  Asset instances
 909       *
 910       * @return  array
 911       *
 912       * @since   4.0.0
 913       */
 914      protected function getConnectionsGraph(array $assets): array
 915      {
 916          $graphOutgoing = [];
 917          $graphIncoming = [];
 918  
 919          foreach ($assets as $asset) {
 920              $name = $asset->getName();
 921  
 922              // Initialise an array for outgoing nodes of the asset
 923              $graphOutgoing[$name] = [];
 924  
 925              // Initialise an array for incoming nodes of the asset
 926              if (!\array_key_exists($name, $graphIncoming)) {
 927                  $graphIncoming[$name] = [];
 928              }
 929  
 930              // Collect an outgoing/incoming nodes
 931              foreach ($asset->getDependencies() as $depName) {
 932                  $graphOutgoing[$name][$depName] = $depName;
 933                  $graphIncoming[$depName][$name] = $name;
 934              }
 935          }
 936  
 937          return [
 938              'outgoing' => $graphOutgoing,
 939              'incoming' => $graphIncoming,
 940          ];
 941      }
 942  
 943      /**
 944       * Return dependencies for Asset as array of WebAssetItem objects
 945       *
 946       * @param   string        $type           The asset type, script or style
 947       * @param   WebAssetItem  $asset          Asset instance
 948       * @param   boolean       $recursively    Whether to search for dependency recursively
 949       * @param   string        $recursionType  The type of initial item to prevent loop
 950       * @param   WebAssetItem  $recursionRoot  Initial item to prevent loop
 951       *
 952       * @return  array
 953       *
 954       * @throws  UnsatisfiedDependencyException When Dependency cannot be found
 955       *
 956       * @since   4.0.0
 957       */
 958      protected function getDependenciesForAsset(
 959          string $type,
 960          WebAssetItem $asset,
 961          $recursively = false,
 962          string $recursionType = null,
 963          WebAssetItem $recursionRoot = null
 964      ): array {
 965          $assets        = [];
 966          $recursionRoot = $recursionRoot ?? $asset;
 967          $recursionType = $recursionType ?? $type;
 968  
 969          foreach ($asset->getDependencies() as $depName) {
 970              $depType = $type;
 971  
 972              // Skip already loaded in recursion
 973              if ($recursionRoot->getName() === $depName && $recursionType === $depType) {
 974                  continue;
 975              }
 976  
 977              if (!$this->registry->exists($depType, $depName)) {
 978                  throw new UnsatisfiedDependencyException(
 979                      sprintf('Unsatisfied dependency "%s" for an asset "%s" of type "%s"', $depName, $asset->getName(), $depType)
 980                  );
 981              }
 982  
 983              $dep = $this->registry->get($depType, $depName);
 984  
 985              $assets[$depType][$depName] = $dep;
 986  
 987              if (!$recursively) {
 988                  continue;
 989              }
 990  
 991              $parentDeps = $this->getDependenciesForAsset($depType, $dep, true, $recursionType, $recursionRoot);
 992              $assets     = array_replace_recursive($assets, $parentDeps);
 993          }
 994  
 995          return $assets;
 996      }
 997  }


Generated: Wed Sep 7 05:41:13 2022 Chilli.vc Blog - For Webmaster,Blog-Writer,System Admin and Domainer