diff --git a/libraries/src/Application/AdministratorApplication.php b/libraries/src/Application/AdministratorApplication.php index e445845a3b7fb..0e6dfcfc3bdc4 100644 --- a/libraries/src/Application/AdministratorApplication.php +++ b/libraries/src/Application/AdministratorApplication.php @@ -99,7 +99,7 @@ public function dispatch($component = null) $this->set('themeParams', $template->params); // Add Asset registry files - $document->getWebAssetManager() + $document->getWebAssetManager()->getRegistry() ->addRegistryFile('media/' . $component . '/joomla.asset.json') ->addRegistryFile('administrator/templates/' . $template->template . '/joomla.asset.json'); diff --git a/libraries/src/Application/SiteApplication.php b/libraries/src/Application/SiteApplication.php index f2d9bce87993e..705a38cb5d8e4 100644 --- a/libraries/src/Application/SiteApplication.php +++ b/libraries/src/Application/SiteApplication.php @@ -184,7 +184,7 @@ public function dispatch($component = null) $this->set('themeParams', $template->params); // Add Asset registry files - $document->getWebAssetManager() + $document->getWebAssetManager()->getRegistry() ->addRegistryFile('media/' . $component . '/joomla.asset.json') ->addRegistryFile('templates/' . $template->template . '/joomla.asset.json'); diff --git a/libraries/src/Document/Document.php b/libraries/src/Document/Document.php index d851ee5713c87..a46087c4e31b1 100644 --- a/libraries/src/Document/Document.php +++ b/libraries/src/Document/Document.php @@ -13,7 +13,7 @@ use Joomla\Application\AbstractWebApplication; use Joomla\CMS\Date\Date; use Joomla\CMS\Factory as CmsFactory; -use Joomla\CMS\WebAsset\WebAssetRegistry; +use Joomla\CMS\WebAsset\WebAssetManager; use Symfony\Component\WebLink\HttpHeaderSerializer; /** @@ -249,7 +249,7 @@ class Document /** * Web Asset instance * - * @var WebAssetRegistry + * @var WebAssetManager * @since 4.0.0 */ protected $webAssetManager = null; @@ -321,13 +321,16 @@ public function __construct($options = array()) $this->setPreloadManager(new PreloadManager); } - if (array_key_exists('webAsset', $options)) + if (array_key_exists('webAssetManager', $options)) { - $this->setWebAssetManager($options['webAsset']); + $this->setWebAssetManager($options['webAssetManager']); } else { - $this->setWebAssetManager(\Joomla\CMS\Factory::getContainer()->get('webasset')); + $webAssetManager = new WebAssetManager(\Joomla\CMS\Factory::getContainer()->get('webassetregistry')); + $webAssetManager->setDispatcher(CmsFactory::getApplication()->getDispatcher()); + + $this->setWebAssetManager($webAssetManager); } } @@ -831,13 +834,13 @@ public function getPreloadManager(): PreloadManagerInterface /** * Set WebAsset manager * - * @param WebAssetRegistry $webAsset The WebAsset instance + * @param WebAssetManager $webAsset The WebAsset instance * * @return Document * * @since 4.0.0 */ - public function setWebAssetManager(WebAssetRegistry $webAsset): self + public function setWebAssetManager(WebAssetManager $webAsset): self { $this->webAssetManager = $webAsset; @@ -847,11 +850,11 @@ public function setWebAssetManager(WebAssetRegistry $webAsset): self /** * Return WebAsset manager * - * @return WebAssetRegistry + * @return WebAssetManager * * @since 4.0.0 */ - public function getWebAssetManager(): WebAssetRegistry + public function getWebAssetManager(): WebAssetManager { return $this->webAssetManager; } diff --git a/libraries/src/Event/WebAsset/AbstractEvent.php b/libraries/src/Event/WebAsset/AbstractEvent.php index 574953a7f6af5..11ab8901c5fc4 100644 --- a/libraries/src/Event/WebAsset/AbstractEvent.php +++ b/libraries/src/Event/WebAsset/AbstractEvent.php @@ -12,7 +12,6 @@ use BadMethodCallException; use Joomla\CMS\Event\AbstractImmutableEvent; -use Joomla\CMS\WebAsset\WebAssetRegistry; /** * Event class for WebAsset events @@ -40,25 +39,4 @@ public function __construct($name, array $arguments = array()) parent::__construct($name, $arguments); } - - /** - * Setter for the subject argument - * - * @param WebAssetRegistry $value The value to set - * - * @return WebAssetRegistry - * - * @throws BadMethodCallException if the argument is not of the expected type - * - * @since 4.0.0 - */ - protected function setSubject($value) - { - if (!$value || !($value instanceof WebAssetRegistry)) - { - throw new BadMethodCallException("Argument 'subject' of event {$this->name} is not of the expected type"); - } - - return $value; - } } diff --git a/libraries/src/Event/WebAsset/WebAssetBeforeAttachEvent.php b/libraries/src/Event/WebAsset/WebAssetBeforeAttachEvent.php index 119fd8eeccf2f..3a854e3b3eb05 100644 --- a/libraries/src/Event/WebAsset/WebAssetBeforeAttachEvent.php +++ b/libraries/src/Event/WebAsset/WebAssetBeforeAttachEvent.php @@ -12,6 +12,7 @@ use BadMethodCallException; use Joomla\CMS\Document\Document; +use Joomla\CMS\WebAsset\WebAssetManager; /** * Event class for WebAsset events @@ -41,6 +42,27 @@ public function __construct($name, array $arguments = array()) parent::__construct($name, $arguments); } + /** + * Setter for the subject argument + * + * @param WebAssetManager $value The value to set + * + * @return WebAssetManager + * + * @throws BadMethodCallException if the argument is not of the expected type + * + * @since __DEPLOY_VERSION__ + */ + protected function setSubject($value) + { + if (!$value || !($value instanceof WebAssetManager)) + { + throw new BadMethodCallException("Argument 'subject' of event {$this->name} is not of the expected type"); + } + + return $value; + } + /** * Return target Document * diff --git a/libraries/src/Event/WebAsset/WebAssetStateChangedEvent.php b/libraries/src/Event/WebAsset/WebAssetStateChangedEvent.php deleted file mode 100644 index 639947030a056..0000000000000 --- a/libraries/src/Event/WebAsset/WebAssetStateChangedEvent.php +++ /dev/null @@ -1,87 +0,0 @@ -arguments['asset']; - } - - /** - * Get previous state of the asset - * - * @return int - * - * @since 4.0.0 - */ - public function getOldState(): int - { - return (int) $this->arguments['oldState']; - } - - /** - * Get new state of the asset - * - * @return int - * - * @since 4.0.0 - */ - public function getNewState(): int - { - return (int) $this->arguments['newState']; - } -} diff --git a/libraries/src/Factory.php b/libraries/src/Factory.php index d85c9ba75a924..97128c0180864 100644 --- a/libraries/src/Factory.php +++ b/libraries/src/Factory.php @@ -559,7 +559,7 @@ protected static function createContainer(): Container ->registerServiceProvider(new \Joomla\CMS\Service\Provider\HTMLRegistry) ->registerServiceProvider(new \Joomla\CMS\Service\Provider\Session) ->registerServiceProvider(new \Joomla\CMS\Service\Provider\Toolbar) - ->registerServiceProvider(new \Joomla\CMS\Service\Provider\WebAsset); + ->registerServiceProvider(new \Joomla\CMS\Service\Provider\WebAssetRegistry); return $container; } diff --git a/libraries/src/Service/Provider/WebAsset.php b/libraries/src/Service/Provider/WebAssetRegistry.php similarity index 75% rename from libraries/src/Service/Provider/WebAsset.php rename to libraries/src/Service/Provider/WebAssetRegistry.php index fe03a630ef280..2182accc17d58 100644 --- a/libraries/src/Service/Provider/WebAsset.php +++ b/libraries/src/Service/Provider/WebAssetRegistry.php @@ -10,7 +10,7 @@ defined('JPATH_PLATFORM') or die; -use Joomla\CMS\WebAsset\WebAssetRegistry; +use Joomla\CMS\WebAsset\WebAssetRegistry as Registry; use Joomla\DI\Container; use Joomla\DI\ServiceProviderInterface; @@ -19,7 +19,7 @@ * * @since 4.0.0 */ -class WebAsset implements ServiceProviderInterface +class WebAssetRegistry implements ServiceProviderInterface { /** * Registers the service provider with a DI container. @@ -32,15 +32,12 @@ class WebAsset implements ServiceProviderInterface */ public function register(Container $container) { - $container->alias('webasset', WebAssetRegistry::class) + $container->alias('webassetregistry', Registry::class) ->share( - WebAssetRegistry::class, + Registry::class, function (Container $container) { - $registry = new WebAssetRegistry; - - // Set up Dispatcher - $registry->setDispatcher($container->get('Joomla\Event\DispatcherInterface')); + $registry = new Registry; // Add Core registry files $registry->addRegistryFile('media/vendor/joomla.asset.json') diff --git a/libraries/src/WebAsset/Exception/UnknownAssetException.php b/libraries/src/WebAsset/Exception/UnknownAssetException.php new file mode 100644 index 0000000000000..7ee31bfc7725d --- /dev/null +++ b/libraries/src/WebAsset/Exception/UnknownAssetException.php @@ -0,0 +1,35 @@ +dependencies = (array) $data['dependencies']; } + + if (!empty($data['weight'])) + { + $this->weight = (float) $data['weight']; + } } /** @@ -187,7 +156,7 @@ public function getVersion() } /** - * Return dependency + * Return dependencies list * * @return array * @@ -199,47 +168,8 @@ public function getDependencies(): array } /** - * Set asset State - * - * @param int $state The asset state - * - * @return self - * - * @since 4.0.0 - */ - public function setState(int $state): self - { - $this->state = $state; - - return $this; - } - - /** - * Get asset State - * - * @return integer - * - * @since 4.0.0 - */ - public function getState(): int - { - return $this->state; - } - - /** - * Check asset state - * - * @return boolean - * - * @since 4.0.0 - */ - public function isActive(): bool - { - return $this->state !== self::ASSET_STATE_INACTIVE; - } - - /** - * Set the Asset weight. Final weight recalculated by AssetFactory. + * Set the desired weight for the Asset in Graph. + * Final weight will be calculated by AssetManager according to dependency Graph. * * @param float $weight The asset weight * @@ -247,7 +177,7 @@ public function isActive(): bool * * @since 4.0.0 */ - public function setWeight(float $weight): self + public function setWeight(float $weight): WebAssetItemInterface { $this->weight = $weight; @@ -255,7 +185,7 @@ public function setWeight(float $weight): self } /** - * Return current weight of the Asset. Final weight recalculated by AssetFactory. + * Return the weight of the Asset. * * @return float * @@ -269,7 +199,7 @@ public function getWeight(): float /** * Get CSS files * - * @param boolean $resolvePath Whether need to search for real path + * @param boolean $resolvePath Whether need to search for a real paths * * @return array * @@ -306,7 +236,7 @@ public function getStylesheetFiles($resolvePath = true): array /** * Get JS files * - * @param boolean $resolvePath Whether we need to search for real path + * @param boolean $resolvePath Whether we need to search for a real paths * * @return array * @@ -340,21 +270,6 @@ public function getScriptFiles($resolvePath = true): array return $this->js; } - /** - * Return list of the asset files, and it's attributes - * - * @return array - * - * @since 4.0.0 - */ - public function getAssetFiles(): array - { - return [ - 'script' => $this->getScriptFiles(true), - 'stylesheet' => $this->getStylesheetFiles(true), - ]; - } - /** * Resolve path * diff --git a/libraries/src/WebAsset/WebAssetItemInterface.php b/libraries/src/WebAsset/WebAssetItemInterface.php new file mode 100644 index 0000000000000..75d19b72e0019 --- /dev/null +++ b/libraries/src/WebAsset/WebAssetItemInterface.php @@ -0,0 +1,90 @@ + State + * + * @var array + * + * @since __DEPLOY_VERSION__ + */ + protected $activeAssets = []; + + /** + * Whether append asset version to asset path + * + * @var bool + * + * @since __DEPLOY_VERSION__ + */ + protected $useVersioning = true; + + /** + * Class constructor + * + * @param WebAssetRegistry $registry The WebAsset Registry instance + * + * @since __DEPLOY_VERSION__ + */ + public function __construct(WebAssetRegistry $registry) + { + $this->registry = $registry; + } + + /** + * Get associated registry instance + * + * @return WebAssetRegistry + * + * @since __DEPLOY_VERSION__ + */ + public function getRegistry(): WebAssetRegistry + { + return $this->registry; + } + + /** + * Activate the Asset item + * + * @param string $name The asset name + * + * @return self + * + * @since __DEPLOY_VERSION__ + */ + public function enableAsset(string $name): WebAssetManagerInterface + { + $asset = $this->registry->get($name); + + // Asset already enabled + if (!empty($this->activeAssets[$name])) + { + // Set state to active, in case it was ASSET_STATE_DEPENDENCY + $this->activeAssets[$name] = static::ASSET_STATE_ACTIVE; + + return $this; + } + + $this->activeAssets[$name] = static::ASSET_STATE_ACTIVE; + + $this->enableDependencies($asset); + + return $this; + } + + /** + * Deactivate the Asset item + * + * @param string $name The asset name + * + * @return self + * + * @since __DEPLOY_VERSION__ + */ + public function disableAsset(string $name): WebAssetManagerInterface + { + unset($this->activeAssets[$name]); + + // Re-check dependencies + $this->enableDependencies(); + + return $this; + } + + /** + * Get a state for the Asset + * + * @param string $name The asset name + * + * @return int + * + * @since __DEPLOY_VERSION__ + */ + public function getAssetState(string $name): int + { + // Check whether asset exists first + $this->registry->get($name); + + if (!empty($this->activeAssets[$name])) + { + return $this->activeAssets[$name]; + } + + return static::ASSET_STATE_INACTIVE; + } + + /** + * Check whether the asset are enabled + * + * @param string $name The asset name + * + * @return bool + * + * @since __DEPLOY_VERSION__ + */ + public function isAssetActive(string $name): bool + { + return $this->getAssetState($name) !== static::ASSET_STATE_INACTIVE; + } + + /** + * Get all assets that was enabled + * + * @param bool $sort Whether need to sort the assets to follow the dependency Graph + * + * @return WebAssetItem[] + * + * @since __DEPLOY_VERSION__ + */ + public function getAssets(bool $sort = false): array + { + if ($sort) + { + return $this->calculateOrderOfActiveAssets(); + } + + $assets = []; + + foreach (array_keys($this->activeAssets) as $name) + { + $assets[$name] = $this->registry->get($name); + } + + return $assets; + } + + /** + * Update Dependencies state for all active Assets or only for given + * + * @param WebAssetItem $asset The asset instance to which need to enable dependencies + * + * @return self + * + * @since __DEPLOY_VERSION__ + */ + protected function enableDependencies(WebAssetItem $asset = null): self + { + if ($asset) + { + $allDependencies = $this->getDependenciesForAsset($asset, true); + + foreach ($allDependencies as $depItem) + { + // Set dependency state only when it is inactive, to keep a manually activated Asset in their original state + if (empty($this->activeAssets[$depItem->getName()])) + { + $this->activeAssets[$depItem->getName()] = static::ASSET_STATE_DEPENDENCY; + } + } + } + else + { + // Re-Check for Dependencies for all active assets + $this->activeAssets = array_filter( + $this->activeAssets, + function ($state){ + return $state === WebAssetManager::ASSET_STATE_ACTIVE; + } + ); + + foreach (array_keys($this->activeAssets) as $name) + { + $asset = $this->registry->get($name); + $this->enableDependencies($asset); + } + } + + return $this; + } + + /** + * Attach active assets to the document + * + * @param Document $doc Document for attach StyleSheet/JavaScript + * + * @return self + * + * @since __DEPLOY_VERSION__ + */ + public function attachActiveAssetsToDocument(Document $doc): WebAssetManagerInterface + { + // Trigger the event + if ($this->getDispatcher()) + { + $event = AbstractEvent::create( + 'onWebAssetBeforeAttach', + [ + 'eventClass' => 'Joomla\\CMS\\Event\\WebAsset\\WebAssetBeforeAttachEvent', + 'subject' => $this, + 'document' => $doc, + ] + ); + $this->getDispatcher()->dispatch($event->getName(), $event); + } + + // Resolve an Order of Assets and their Dependencies + $assets = $this->enableDependencies()->getAssets(true); + + // Pre-save existing Scripts, and attach them after requested assets. + $jsBackup = $doc->_scripts; + $doc->_scripts = []; + + // Attach active assets to the document + foreach ($assets as $asset) + { + // Add StyleSheets of the asset + foreach ($asset->getStylesheetFiles(true) as $path => $attr) + { + unset($attr['__isExternal'], $attr['__pathOrigin']); + $version = $this->useVersioning ? ($asset->getVersion() ?: 'auto') : false; + $doc->addStyleSheet($path, ['version' => $version], $attr); + } + + // Add Scripts of the asset + foreach ($asset->getScriptFiles(true) as $path => $attr) + { + unset($attr['__isExternal'], $attr['__pathOrigin']); + $version = $this->useVersioning ? ($asset->getVersion() ?: 'auto') : false; + $doc->addScript($path, ['version' => $version], $attr); + } + } + + // Merge with previously added scripts + $doc->_scripts = array_replace($doc->_scripts, $jsBackup); + + return $this; + } + + /** + * Calculate weight of active Assets, by its Dependencies + * + * @return WebAssetItem[] + * + * @since __DEPLOY_VERSION__ + */ + protected function calculateOrderOfActiveAssets(): array + { + // See https://en.wikipedia.org/wiki/Topological_sorting#Kahn.27s_algorithm + $graphOrder = []; + $activeAssets = $this->getAssets(); + + // Get Graph of Outgoing and Incoming connections + $connectionsGraph = $this->getConnectionsGraph($activeAssets); + $graphOutgoing = $connectionsGraph['outgoing']; + $graphIncoming = $connectionsGraph['incoming']; + + // Make a copy to be used during weight processing + $graphIncomingCopy = $graphIncoming; + + // Find items without incoming connections + $emptyIncoming = array_keys( + array_filter( + $graphIncoming, + function ($el){ + return !$el; + } + ) + ); + + // Loop through, and sort the graph + while ($emptyIncoming) + { + // Add the node without incoming connection to the result + $item = array_shift($emptyIncoming); + $graphOrder[] = $item; + + // Check of each neighbor of the node + foreach (array_reverse($graphOutgoing[$item]) as $neighbor) + { + // Remove incoming connection of already visited node + unset($graphIncoming[$neighbor][$item]); + + // If there no more incoming connections add the node to queue + if (empty($graphIncoming[$neighbor])) + { + $emptyIncoming[] = $neighbor; + } + } + } + + // Sync Graph order with FIFO order + $fifoWeights = []; + $graphWeights = []; + $requestedWeights = []; + + foreach (array_keys($this->activeAssets) as $index => $name) + { + $fifoWeights[$name] = $index * 10 + 10; + } + + foreach (array_reverse($graphOrder) as $index => $name) + { + $graphWeights[$name] = $index * 10 + 10; + $requestedWeights[$name] = $activeAssets[$name]->getWeight() ?: $fifoWeights[$name]; + } + + // Try to set a requested weight, or make it close as possible to requested, but keep the Graph order + while ($requestedWeights) + { + $item = key($requestedWeights); + $weight = array_shift($requestedWeights); + + // Skip empty items + if ($weight === null) + { + continue; + } + + // Check the predecessors (Outgoing vertexes), the weight cannot be lighter than the predecessor have + $topBorder = $weight - 1; + if (!empty($graphOutgoing[$item])) + { + $prevWeights = []; + foreach ($graphOutgoing[$item] as $pItem) + { + $prevWeights[] = $graphWeights[$pItem]; + } + $topBorder = max($prevWeights); + } + + // Calculate a new weight + $newWeight = $weight > $topBorder ? $weight : $topBorder + 1; + + // If a new weight heavier than existing, then we need to update all incoming connections (children) + if ($newWeight > $graphWeights[$item] && !empty($graphIncomingCopy[$item])) + { + // Sort Graph of incoming by actual position + foreach ($graphIncomingCopy[$item] as $incomingItem) + { + // Set a weight heavier than current, then this node to be processed in next iteration + if (empty($requestedWeights[$incomingItem])) + { + $requestedWeights[$incomingItem] = $graphWeights[$incomingItem] + $newWeight; + } + } + } + + // Set a new weight + $graphWeights[$item] = $newWeight; + } + + asort($graphWeights); + + // Get Assets in calculated order + $resultAssets = []; + foreach (array_keys($graphWeights) as $name) + { + $resultAssets[$name] = $activeAssets[$name]; + } + + return $resultAssets; + } + + /** + * Build Graph of Outgoing and Incoming connections for given assets. + * + * @param WebAssetItem[] $assets Asset instances + * + * @return array + * + * @since __DEPLOY_VERSION__ + */ + protected function getConnectionsGraph (array $assets): array + { + $graphOutgoing = []; + $graphIncoming = []; + + foreach ($assets as $asset) + { + $name = $asset->getName(); + $graphOutgoing[$name] = array_combine($asset->getDependencies(), $asset->getDependencies()); + + if (!array_key_exists($name, $graphIncoming)) + { + $graphIncoming[$name] = []; + } + + foreach ($asset->getDependencies() as $depName) + { + $graphIncoming[$depName][$name] = $name; + } + } + + return [ + 'outgoing' => $graphOutgoing, + 'incoming' => $graphIncoming, + ]; + } + + /** + * Return dependencies for Asset as array of WebAssetItem objects + * + * @param WebAssetItem $asset Asset instance + * @param boolean $recursively Whether to search for dependancy recursively + * @param WebAssetItem $recursionRoot Initial item to prevent loop + * + * @return WebAssetItem[] + * + * @throws UnsatisfiedDependencyException When Dependency cannot be found + * + * @since __DEPLOY_VERSION__ + */ + protected function getDependenciesForAsset(WebAssetItem $asset, $recursively = false, WebAssetItem $recursionRoot = null): array + { + $assets = []; + $recursionRoot = $recursionRoot ?? $asset; + + foreach ($asset->getDependencies() as $depName) + { + // Skip already loaded in recursion + if ($recursionRoot->getName() === $depName) + { + continue; + } + + if (!$this->registry->exists($depName)) + { + throw new UnsatisfiedDependencyException('Unsatisfied dependency "' . $depName . '" for Asset "' . $asset->getName() . '"'); + } + + $dep = $this->registry->get($depName); + + $assets[$depName] = $dep; + + if (!$recursively) + { + continue; + } + + $parentDeps = $this->getDependenciesForAsset($dep, true, $recursionRoot); + $assets = array_replace($assets, $parentDeps); + } + + return $assets; + } + + /** + * Whether append asset version to asset path + * + * @param bool $useVersioning Boolean flag + * + * @return self + * + * @since __DEPLOY_VERSION__ + */ + public function useVersioning(bool $useVersioning): self + { + $this->useVersioning = $useVersioning; + + return $this; + } + + /** + * Dump available assets to simple array, with some basic info + * + * @return array + * + * @since __DEPLOY_VERSION__ + */ + public function debugAssets(): array + { + // Update dependencies + $assets = $this->enableDependencies()->getAssets(true); + $result = []; + + foreach ($assets as $asset) + { + $result[$asset->getName()] = [ + 'name' => $asset->getName(), + 'deps' => implode(', ', $asset->getDependencies()), + 'state' => $this->getAssetState($asset->getName()), + ]; + } + + return $result; + } +} diff --git a/libraries/src/WebAsset/WebAssetManagerInterface.php b/libraries/src/WebAsset/WebAssetManagerInterface.php new file mode 100644 index 0000000000000..18e39a016d543 --- /dev/null +++ b/libraries/src/WebAsset/WebAssetManagerInterface.php @@ -0,0 +1,78 @@ +parseRegistryFiles(); - if (!empty($this->assets[$name])) + if (empty($this->assets[$name])) { - return $this->assets[$name]; + throw new UnknownAssetException($name); } - return null; - } - - /** - * Search for all active assets. - * - * @return WebAssetItem[] Array with active assets - * - * @since 4.0.0 - */ - public function getActiveAssets(): array - { - $assets = array_filter( - $this->assets, - function($asset) - { - /** @var WebAssetItem $asset */ - return $asset->isActive(); - } - ); - - return $assets; - } - - /** - * Search for assets with specific state. - * - * @param int $state Asset state - * - * @return WebAssetItem[] Array with active assets - * - * @since 4.0.0 - */ - public function getAssetsByState(int $state = WebAssetItem::ASSET_STATE_ACTIVE): array - { - $assets = array_filter( - $this->assets, - function($asset) use ($state) - { - /** @var WebAssetItem $asset */ - return $asset->getState() === $state; - } - ); - - return $assets; + return $this->assets[$name]; } /** * Add Asset to registry of known assets * - * @param WebAssetItem $asset Asset instance + * @param WebAssetItemInterface $asset Asset instance * * @return self * * @since 4.0.0 */ - public function addAsset(WebAssetItem $asset): self + public function add(WebAssetItemInterface $asset): WebAssetRegistryInterface { - // Check whether the asset already exists, so we must copy its state before override - if (!empty($this->assets[$asset->getName()])) - { - $existing = $this->assets[$asset->getName()]; - $asset->setState($existing->getState()); - } - $this->assets[$asset->getName()] = $asset; return $this; @@ -226,351 +135,25 @@ public function addAsset(WebAssetItem $asset): self * * @since 4.0.0 */ - public function removeAsset(string $name): self - { - if (!empty($this->assets[$name])) - { - unset($this->assets[$name]); - } - - return $this; - } - - /** - * Change the asset State - * - * @param string $name Asset name - * @param integer $state New state - * - * @return self - * - * @throws \RuntimeException if asset with given name does not exist - * - * @since 4.0.0 - */ - public function setAssetState(string $name, int $state = WebAssetItem::ASSET_STATE_ACTIVE): self - { - $asset = $this->getAsset($name); - - if (!$asset) - { - throw new \RuntimeException('Asset "' . $name . '" does not exist'); - } - - $currentState = $asset->getState(); - - // Asset already has the requested state - if ($currentState === $state) - { - return $this; - } - - // Change state - $asset->setState($state); - - // Update Dependency - $this->updateDependency(); - - // Trigger the event - $event = AbstractEvent::create( - 'onWebAssetStateChangedExternally', - [ - 'eventClass' => 'Joomla\\CMS\\Event\\WebAsset\\WebAssetStateChangedEvent', - 'subject' => $this, - 'asset' => $asset, - 'oldState' => $currentState, - 'newState' => $state, - ] - ); - $this->getDispatcher()->dispatch($event->getName(), $event); - - return $this; - } - - /** - * Activate the Asset item - * - * @param string $name The asset name - * - * @return self - * - * @since 4.0.0 - */ - public function enableAsset(string $name): self - { - return $this->setAssetState($name, WebAssetItem::ASSET_STATE_ACTIVE); - } - - /** - * Deactivate the Asset item - * - * @param string $name The asset name - * - * @return self - * - * @since 4.0.0 - */ - public function disableAsset(string $name): self - { - return $this->setAssetState($name, WebAssetItem::ASSET_STATE_INACTIVE); - } - - /** - * Attach active assets to the document - * - * @param Document $doc Document for attach StyleSheet/JavaScript - * - * @return self - * - * @since 4.0.0 - */ - public function attachActiveAssetsToDocument(Document $doc): self - { - // Resolve Dependency - $this->updateDependency()->calculateWeightOfActiveAssets(); - - // Trigger the event - $event = AbstractEvent::create( - 'onWebAssetBeforeAttach', - [ - 'eventClass' => 'Joomla\\CMS\\Event\\WebAsset\\WebAssetBeforeAttachEvent', - 'subject' => $this, - 'document' => $doc, - ] - ); - $this->getDispatcher()->dispatch($event->getName(), $event); - - $assets = $this->sortAssetsByWeight($this->getActiveAssets()); - - // Pre-save existing Scripts, and attach them after requested assets. - $jsBackup = $doc->_scripts; - $doc->_scripts = []; - - // Attach active assets to the document - foreach ($assets as $asset) - { - $paths = $asset->getAssetFiles(); - - // Add StyleSheets of the asset - foreach ($paths['stylesheet'] as $path => $attr) - { - unset($attr['__isExternal'], $attr['__pathOrigin']); - $version = $this->useVersioning ? ($asset->getVersion() ?: 'auto') : false; - $doc->addStyleSheet($path, ['version' => $version], $attr); - } - - // Add Scripts of the asset - foreach ($paths['script'] as $path => $attr) - { - unset($attr['__isExternal'], $attr['__pathOrigin']); - $version = $this->useVersioning ? ($asset->getVersion() ?: 'auto') : false; - $doc->addScript($path, ['version' => $version], $attr); - } - } - - // Merge with previously added scripts - $doc->_scripts = array_replace($doc->_scripts, $jsBackup); - - return $this; - } - - /** - * Update Dependencies state for all active Assets - * - * @return self - * - * @since 4.0.0 - */ - protected function updateDependency(): self - { - // First, deactivate all Dependency - foreach ($this->getAssetsByState(WebAssetItem::ASSET_STATE_DEPENDANCY) as $depItem) - { - $depItem->setState(WebAssetItem::ASSET_STATE_INACTIVE); - } - - // Second, get list of active assets and enable their dependencies - $assets = $this->getAssetsByState(WebAssetItem::ASSET_STATE_ACTIVE); - - foreach ($assets as $asset) - { - $this->updateItemDependency($asset); - } - - return $this; - } - - /** - * Update Dependencies state for given Asset - * - * @param WebAssetItem $asset Asset instance - * - * @return self - * - * @throws \RuntimeException When Dependency cannot be resolved - * - * @since 4.0.0 - */ - protected function updateItemDependency(WebAssetItem $asset): self + public function remove(string $name): WebAssetRegistryInterface { - foreach ($this->getDependenciesForAsset($asset, true) as $depItem) - { - // Set dependency state only when it is inactive, to keep a manually activated Asset in their original state - if (!$depItem->isActive()) - { - $depItem->setState(WebAssetItem::ASSET_STATE_DEPENDANCY); - } - } + unset($this->assets[$name]); return $this; } /** - * Calculate weight of active Assets, by its Dependencies - * - * @return self + * Check whether the asset exists in the registry. * - * @since 4.0.0 - */ - protected function calculateWeightOfActiveAssets(): self - { - // See https://en.wikipedia.org/wiki/Topological_sorting#Kahn.27s_algorithm - $result = []; - $graphOutgoing = []; - $graphIncoming = []; - $activeAssets = $this->getActiveAssets(); - - // Build Graphs of Outgoing and Incoming connections - foreach ($activeAssets as $asset) - { - $name = $asset->getName(); - $graphOutgoing[$name] = array_combine($asset->getDependencies(), $asset->getDependencies()); - - if (!array_key_exists($name, $graphIncoming)) - { - $graphIncoming[$name] = []; - } - - foreach ($asset->getDependencies() as $depName) - { - $graphIncoming[$depName][$name] = $name; - } - } - - // Find items without incoming connections - $emptyIncoming = array_keys( - array_filter( - $graphIncoming, - function ($el){ - return !$el; - } - ) - ); - - // Loop through, and sort the graph - while ($emptyIncoming) - { - // Add the node without incoming connection to the result - $item = array_shift($emptyIncoming); - $result[] = $item; - - // Check of each neighbor of the node - foreach (array_reverse($graphOutgoing[$item]) as $neighbor) - { - // Remove incoming connection of already visited node - unset($graphIncoming[$neighbor][$item]); - - // If there no more incoming connections add the node to queue - if (empty($graphIncoming[$neighbor])) - { - $emptyIncoming[] = $neighbor; - } - } - } - - // Update a weight for each active asset - foreach (array_reverse($result) as $index => $name) - { - $activeAssets[$name]->setWeight($index + 1); - } - - return $this; - } - - /** - * Return dependancy for Asset as array of AssetItem objects - * - * @param WebAssetItem $asset Asset instance - * @param boolean $recursively Whether to search for dependancy recursively - * @param WebAssetItem $recursionRoot Initial item to prevent loop - * - * @return WebAssetItem[] - * - * @throws \RuntimeException When Dependency cannot be found - * - * @since 4.0.0 - */ - protected function getDependenciesForAsset(WebAssetItem $asset, $recursively = false, WebAssetItem $recursionRoot = null): array - { - $assets = []; - $recursionRoot = $recursionRoot ?? $asset; - - foreach ($asset->getDependencies() as $depName) - { - // Skip already loaded in recursion - if ($recursionRoot->getName() === $depName) - { - continue; - } - - $dep = $this->getAsset($depName); - - if (!$dep) - { - throw new \RuntimeException('Cannot find Dependency "' . $depName . '" for Asset "' . $asset->getName() . '"'); - } - - $assets[$depName] = $dep; - - if (!$recursively) - { - continue; - } - - $parentDeps = $this->getDependenciesForAsset($dep, true, $recursionRoot); - $assets = array_replace($assets, $parentDeps); - } - - return $assets; - } - - /** - * Sort assets by its weight - * - * @param WebAssetItem[] $assets Array of assets to sort + * @param string $name Asset name * - * @return WebAssetItem[] + * @return bool * - * @since 4.0.0 + * @since __DEPLOY_VERSION__ */ - public function sortAssetsByWeight(array $assets): array + public function exists(string $name): bool { - uasort( - $assets, - function($a, $b) - { - /** @var WebAssetItem $a */ - /** @var WebAssetItem $b */ - if ($a->getWeight() === $b->getWeight()) - { - return 0; - } - - return $a->getWeight() > $b->getWeight() ? 1 : -1; - } - ); - - return $assets; + return !empty($this->assets[$name]); } /** @@ -601,12 +184,15 @@ public function addRegistryFile(string $path): self { $path = Path::clean($path); - if (isset($this->dataFiles[$path])) + if (isset($this->dataFilesNew[$path]) || isset($this->dataFilesParsed[$path])) { return $this; } - $this->dataFiles[$path] = is_file(JPATH_ROOT . '/' . $path) ? static::REGISTRY_FILE_NEW : static::REGISTRY_FILE_INVALID; + if (is_file(JPATH_ROOT . '/' . $path)) + { + $this->dataFilesNew[$path] = $path; + } return $this; } @@ -620,27 +206,18 @@ public function addRegistryFile(string $path): self */ protected function parseRegistryFiles() { - // Filter new asset data files and parse each - $constantIsNew = static::REGISTRY_FILE_NEW; - $files = array_filter( - $this->dataFiles, - function($state) use ($constantIsNew) - { - return $state === $constantIsNew; - } - ); - - if (!$files) + if (!$this->dataFilesNew) { return; } - foreach (array_keys($files) as $path) + foreach ($this->dataFilesNew as $path) { $this->parseRegistryFile($path); // Mark as parsed (not new) - $this->dataFiles[$path] = static::REGISTRY_FILE_PARSED; + unset($this->dataFilesNew[$path]); + $this->dataFilesParsed[$path] = $path; } } @@ -686,54 +263,7 @@ protected function parseRegistryFile($path) $item['assetSource'] = $assetSource; $assetItem = $this->createAsset($item['name'], $item); - $this->addAsset($assetItem); + $this->add($assetItem); } } - - /** - * Dump available assets to simple array, with some basic info - * - * @param bool $onlyActive Return only active Assets - * - * @return array - * - * @since 4.0.0 - */ - public function debugAssets(bool $onlyActive = false): array - { - // Update dependencies - $this->updateDependency()->calculateWeightOfActiveAssets(); - - $assets = $onlyActive ? $this->getActiveAssets() : $this->assets; - $assets = $this->sortAssetsByWeight($assets); - $result = []; - - foreach ($assets as $asset) - { - $result[$asset->getName()] = [ - 'name' => $asset->getName(), - 'deps' => implode(', ', $asset->getDependencies()), - 'state' => $asset->getState(), - 'weight' => $asset->getWeight(), - ]; - } - - return $result; - } - - /** - * Whether append asset version to asset path - * - * @param bool $useVersioning Boolean flag - * - * @return self - * - * @since 4.0.0 - */ - public function useVersioning(bool $useVersioning): self - { - $this->useVersioning = $useVersioning; - - return $this; - } } diff --git a/libraries/src/WebAsset/WebAssetRegistryInterface.php b/libraries/src/WebAsset/WebAssetRegistryInterface.php new file mode 100644 index 0000000000000..f97d15c371ef4 --- /dev/null +++ b/libraries/src/WebAsset/WebAssetRegistryInterface.php @@ -0,0 +1,69 @@ +