<?php
declare(strict_types=1);
/**
* Pimcore
*
* This source file is available under two different licenses:
* - GNU General Public License version 3 (GPLv3)
* - Pimcore Commercial License (PCL)
* Full copyright and license information is available in
* LICENSE.md which is distributed with this source code.
*
* @copyright Copyright (c) Pimcore GmbH (http://www.pimcore.org)
* @license http://www.pimcore.org/license GPLv3 and PCL
*/
namespace Pimcore\Extension\Bundle;
use Pimcore\Event\BundleManager\PathsEvent;
use Pimcore\Event\BundleManagerEvents;
use Pimcore\Extension\Bundle\Config\StateConfig;
use Pimcore\Extension\Bundle\Exception\BundleNotFoundException;
use Pimcore\Extension\Bundle\Installer\Exception\InstallationException;
use Pimcore\HttpKernel\BundleCollection\ItemInterface;
use Pimcore\Kernel;
use Pimcore\Routing\RouteReferenceInterface;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
/**
* @internal
*/
class PimcoreBundleManager
{
/**
* @var StateConfig
*/
protected $stateConfig;
/**
* @var PimcoreBundleLocator
*/
protected $bundleLocator;
/**
* @var Kernel
*/
protected $kernel;
/**
* @var EventDispatcherInterface
*/
protected $dispatcher;
/**
* @var RouterInterface
*/
protected $router;
/**
* @var array
*/
protected $availableBundles;
/**
* @var array
*/
protected $enabledBundles;
/**
* @var array
*/
protected $manuallyRegisteredBundleState;
/**
* @param StateConfig $stateConfig
* @param PimcoreBundleLocator $bundleLocator
* @param Kernel $kernel
* @param EventDispatcherInterface $dispatcher
* @param RouterInterface $router
*/
public function __construct(
StateConfig $stateConfig,
PimcoreBundleLocator $bundleLocator,
Kernel $kernel,
EventDispatcherInterface $dispatcher,
RouterInterface $router
) {
$this->stateConfig = $stateConfig;
$this->bundleLocator = $bundleLocator;
$this->kernel = $kernel;
$this->dispatcher = $dispatcher;
$this->router = $router;
}
/**
* List of currently active bundles from kernel. A bundle can be in this list without being enabled via extensions
* config file if it is registered manually on the kernel.
*
* @param bool $onlyInstalled
*
* @return PimcoreBundleInterface[]
*/
public function getActiveBundles(bool $onlyInstalled = true): array
{
$bundles = [];
foreach ($this->kernel->getBundles() as $bundle) {
if ($bundle instanceof PimcoreBundleInterface) {
if ($onlyInstalled && !$this->isInstalled($bundle)) {
continue;
}
$bundles[get_class($bundle)] = $bundle;
}
}
return $bundles;
}
/**
* @param string $id
* @param bool $onlyInstalled
*
* @return PimcoreBundleInterface
*/
public function getActiveBundle(string $id, bool $onlyInstalled = true): PimcoreBundleInterface
{
foreach ($this->getActiveBundles($onlyInstalled) as $bundle) {
if ($this->getBundleIdentifier($bundle) === $id) {
return $bundle;
}
}
throw new BundleNotFoundException(sprintf('Bundle %s was not found', $id));
}
/**
* List of available bundles from a defined set of paths
*
* @return array
*/
public function getAvailableBundles(): array
{
if (null === $this->availableBundles) {
$bundles = $this->getManuallyRegisteredBundleNames(false);
foreach ($this->bundleLocator->findBundles() as $locatedBundle) {
if (!in_array($locatedBundle, $bundles)) {
$bundles[] = $locatedBundle;
}
}
sort($bundles);
$this->availableBundles = $bundles;
}
return $this->availableBundles;
}
/**
* Lists enabled bundle names
*
* @return array
*/
public function getEnabledBundleNames(): array
{
$bundleNames = array_merge(
$this->getManuallyRegisteredBundleNames(true),
$this->stateConfig->getEnabledBundleNames()
);
$bundleNames = array_unique($bundleNames);
sort($bundleNames);
return $bundleNames;
}
/**
* Returns names of manually registered bundles (not registered via extension manager)
*
* @param bool $onlyEnabled
*
* @return array
*/
private function getManuallyRegisteredBundleNames(bool $onlyEnabled = false): array
{
$state = $this->getManuallyRegisteredBundleState();
if (!$onlyEnabled) {
return array_keys($state);
}
$bundleNames = [];
foreach ($state as $bundleName => $options) {
if ($options['enabled']) {
$bundleNames[] = $bundleName;
}
}
return $bundleNames;
}
/**
* Builds state infos about manually configured bundles (not registered via extension manager)
*
* @return array
*/
private function getManuallyRegisteredBundleState()
{
if (null === $this->manuallyRegisteredBundleState) {
$collection = $this->kernel->getBundleCollection();
$enabledBundles = array_keys($this->getActiveBundles(false));
$bundles = [];
foreach ($collection->getItems() as $item) {
if (!$item->isPimcoreBundle()) {
continue;
}
if ($item->getSource() === ItemInterface::SOURCE_EXTENSION_MANAGER_CONFIG) {
continue;
}
$bundles[$item->getBundleIdentifier()] = $this->stateConfig->normalizeOptions([
'enabled' => in_array($item->getBundleIdentifier(), $enabledBundles),
'priority' => $item->getPriority(),
'environments' => $item->getEnvironments(),
]);
}
$this->manuallyRegisteredBundleState = $bundles;
}
return $this->manuallyRegisteredBundleState;
}
/**
* Determines if a bundle exists (is enabled or can be enabled)
*
* @param string|PimcoreBundleInterface $bundle
*
* @return bool
*/
public function exists($bundle): bool
{
$identifier = $this->getBundleIdentifier($bundle);
return $this->isValidBundleIdentifier($identifier);
}
/**
* @param string|PimcoreBundleInterface $bundle
*
* @return string
*/
public function getBundleIdentifier($bundle): string
{
$identifier = $bundle;
if ($bundle instanceof PimcoreBundleInterface) {
$identifier = get_class($bundle);
}
return $identifier;
}
/**
* @param string $identifier
*
* @return bool
*/
protected function isValidBundleIdentifier(string $identifier): bool
{
$validNames = array_merge(
array_keys($this->getActiveBundles(false)),
$this->getAvailableBundles()
);
return in_array($identifier, $validNames);
}
/**
* Validates bundle name against list if available and active bundles
*
* @param string $identifier
*/
protected function validateBundleIdentifier(string $identifier)
{
if (!$this->isValidBundleIdentifier($identifier)) {
throw new BundleNotFoundException(sprintf('Bundle "%s" is no valid bundle identifier', $identifier));
}
}
/**
* Determines if the bundle was programatically registered (not via extension manager)
*
* @param string|PimcoreBundleInterface $bundle
*
* @return bool
*/
public function isManuallyRegistered($bundle): bool
{
$identifier = $this->getBundleIdentifier($bundle);
$this->validateBundleIdentifier($identifier);
return in_array($identifier, $this->getManuallyRegisteredBundleNames(false));
}
/**
* Checks if a state change (enable/disable, priority, environments) is possible
*
* @param string $identifier
*/
protected function validateStateChange(string $identifier)
{
if ($this->isManuallyRegistered($identifier)) {
throw new \LogicException(sprintf(
'Can\'t change state for bundle "%s" as it is programatically registered',
$identifier
));
}
}
/**
* Determines if bundle is allowed to change state (can be enabled/disabled)
*
* @param string|PimcoreBundleInterface $bundle
*
* @return bool
*/
public function canChangeState($bundle): bool
{
$identifier = $this->getBundleIdentifier($bundle);
$this->validateBundleIdentifier($identifier);
return !$this->isManuallyRegistered($bundle);
}
/**
* Reads bundle state from config
*
* @param string|PimcoreBundleInterface $bundle
*
* @return array
*/
public function getState($bundle): array
{
$identifier = $this->getBundleIdentifier($bundle);
$this->validateBundleIdentifier($identifier);
if ($this->isManuallyRegistered($identifier)) {
return $this->getManuallyRegisteredBundleState()[$identifier];
}
return $this->stateConfig->getState($identifier);
}
/**
* Updates state for a bundle and writes it to config
*
* @param string|PimcoreBundleInterface $bundle
* @param array $options
*/
public function setState($bundle, array $options)
{
$identifier = $this->getBundleIdentifier($bundle);
$this->validateBundleIdentifier($identifier);
$this->validateStateChange($identifier);
$this->stateConfig->setState($identifier, $options);
}
/**
* Batch updates bundle states
*
* @param array $states
*/
public function setStates(array $states)
{
$updates = [];
foreach ($states as $bundle => $options) {
$identifier = $this->getBundleIdentifier($bundle);
$this->validateBundleIdentifier($identifier);
$this->validateStateChange($identifier);
$updates[$identifier] = $options;
}
$this->stateConfig->setStates($updates);
}
/**
* Enables a bundle
*
* @param string|PimcoreBundleInterface $bundle
* @param array $state Optional additional state config (see StateConfig)
*/
public function enable($bundle, array $state = [])
{
$state = array_merge($state, [
'enabled' => true,
]);
$this->setState($bundle, $state);
}
/**
* Disables a bundle
*
* @param string|PimcoreBundleInterface $bundle
*/
public function disable($bundle)
{
$this->setState($bundle, ['enabled' => false]);
}
/**
* Determines if a bundle is enabled
*
* @param string|PimcoreBundleInterface $bundle
*
* @return bool
*/
public function isEnabled($bundle): bool
{
$identifier = $this->getBundleIdentifier($bundle);
$this->validateBundleIdentifier($identifier);
return in_array($identifier, $this->getEnabledBundleNames());
}
/**
* @param PimcoreBundleInterface $bundle
* @param bool $throwException
*
* @return null|Installer\InstallerInterface
*/
protected function loadBundleInstaller(PimcoreBundleInterface $bundle, bool $throwException = false)
{
if (null === $installer = $bundle->getInstaller()) {
if ($throwException) {
throw new InstallationException(sprintf('Bundle %s does not a define an installer', $bundle->getName()));
}
return null;
}
return $installer;
}
/**
* Returns the bundle installer if configured
*
* @param PimcoreBundleInterface $bundle
* @param bool $throwException
*
* @return null|Installer\InstallerInterface
*/
public function getInstaller(PimcoreBundleInterface $bundle, bool $throwException = false)
{
return $this->loadBundleInstaller($bundle, $throwException);
}
/**
* Runs install routine for a bundle
*
* @param PimcoreBundleInterface $bundle
*
* @throws InstallationException If the bundle can not be installed or doesn't define an installer
*/
public function install(PimcoreBundleInterface $bundle)
{
$installer = $this->loadBundleInstaller($bundle, true);
if (!$this->canBeInstalled($bundle)) {
throw new InstallationException(sprintf('Bundle %s can not be installed', $bundle->getName()));
}
$installer->install();
}
/**
* Runs uninstall routine for a bundle
*
* @param PimcoreBundleInterface $bundle
*
* @throws InstallationException If the bundle can not be uninstalled or doesn't define an installer
*/
public function uninstall(PimcoreBundleInterface $bundle)
{
$installer = $this->loadBundleInstaller($bundle, true);
if (!$this->canBeUninstalled($bundle)) {
throw new InstallationException(sprintf('Bundle %s can not be uninstalled', $bundle->getName()));
}
$installer->uninstall();
}
/**
* Determines if a bundle can be installed
*
* @param PimcoreBundleInterface $bundle
*
* @return bool
*/
public function canBeInstalled(PimcoreBundleInterface $bundle): bool
{
if (!$this->isEnabled($bundle)) {
return false;
}
if (null === $installer = $this->loadBundleInstaller($bundle)) {
return false;
}
return $installer->canBeInstalled();
}
/**
* Determines if a bundle can be uninstalled
*
* @param PimcoreBundleInterface $bundle
*
* @return bool
*/
public function canBeUninstalled(PimcoreBundleInterface $bundle): bool
{
if (!$this->isEnabled($bundle)) {
return false;
}
if (null === $installer = $this->loadBundleInstaller($bundle)) {
return false;
}
return $installer->canBeUninstalled();
}
/**
* Determines if a bundle is installed
*
* @param PimcoreBundleInterface $bundle
*
* @return bool
*/
public function isInstalled(PimcoreBundleInterface $bundle): bool
{
if (null === $installer = $bundle->getInstaller()) {
// bundle has no dedicated installer, so we can treat it as installed
return true;
}
return $installer->isInstalled();
}
/**
* Determines if a reload is needed after installation
*
* @param PimcoreBundleInterface $bundle
*
* @return bool
*/
public function needsReloadAfterInstall(PimcoreBundleInterface $bundle): bool
{
if (null === $installer = $bundle->getInstaller()) {
// bundle has no dedicated installer
return false;
}
return $installer->needsReloadAfterInstall();
}
/**
* Resolves all admin javascripts to load
*
* @return array
*/
public function getJsPaths(): array
{
$paths = $this->resolvePaths('js');
return $this->resolveEventPaths($paths, BundleManagerEvents::JS_PATHS);
}
/**
* Resolves all admin stylesheets to load
*
* @return array
*/
public function getCssPaths(): array
{
$paths = $this->resolvePaths('css');
return $this->resolveEventPaths($paths, BundleManagerEvents::CSS_PATHS);
}
/**
* Resolves all editmode javascripts to load
*
* @return array
*/
public function getEditmodeJsPaths(): array
{
$paths = $this->resolvePaths('js', 'editmode');
return $this->resolveEventPaths($paths, BundleManagerEvents::EDITMODE_JS_PATHS);
}
/**
* Resolves all editmode stylesheets to load
*
* @return array
*/
public function getEditmodeCssPaths(): array
{
$paths = $this->resolvePaths('css', 'editmode');
return $this->resolveEventPaths($paths, BundleManagerEvents::EDITMODE_CSS_PATHS);
}
/**
* Iterates installed bundles and fetches asset paths
*
* @param string $type
* @param string|null $mode
*
* @return array
*/
protected function resolvePaths(string $type, string $mode = null): array
{
$type = ucfirst($type);
if (null !== $mode) {
$mode = ucfirst($mode);
} else {
$mode = '';
}
// getJsPaths, getEditmodeJsPaths
$getter = sprintf('get%s%sPaths', $mode, $type);
$result = [];
foreach ($this->getActiveBundles() as $bundle) {
$paths = $bundle->$getter();
foreach ($paths as $path) {
if ($path instanceof RouteReferenceInterface) {
$result[] = $this->router->generate(
$path->getRoute(),
$path->getParameters(),
$path->getType()
);
} else {
$result[] = $path;
}
}
}
return $result;
}
/**
* Emits given path event
*
* @param array $paths
* @param string $eventName
*
* @return array
*/
protected function resolveEventPaths(array $paths, string $eventName): array
{
$event = new PathsEvent($paths);
$this->dispatcher->dispatch($event, $eventName);
return $event->getPaths();
}
}