Update to Drupal 8.0-dev-2015-11-17. Commits through da81cd220, Tue Nov 17 15:53:49 2015 +0000, Issue #2617224 by Wim Leers: Move around/fix some documentation.

This commit is contained in:
Pantheon Automation 2015-11-17 13:42:33 -08:00 committed by Greg Anderson
parent 4afb23bbd3
commit 7784f4c23d
929 changed files with 19798 additions and 5304 deletions

View file

@ -79,8 +79,7 @@ class FileStorage implements PhpStorageInterface {
public static function htaccessLines($private = TRUE) {
$lines = <<<EOF
# Turn off all options we don't need.
Options None
Options +FollowSymLinks
Options -Indexes -ExecCGI -Includes -MultiViews
# Set the catch-all handler to prevent scripts from being executed.
SetHandler Drupal_Security_Do_Not_Remove_See_SA_2006_006

View file

@ -116,8 +116,9 @@ class AssetResolver implements AssetResolverInterface {
public function getCssAssets(AttachedAssetsInterface $assets, $optimize) {
$theme_info = $this->themeManager->getActiveTheme();
// Add the theme name to the cache key since themes may implement
// hook_css_alter().
$cid = 'css:' . $theme_info->getName() . ':' . Crypt::hashBase64(serialize($assets)) . (int) $optimize;
// hook_library_info_alter().
$libraries_to_load = $this->getLibrariesToLoad($assets);
$cid = 'css:' . $theme_info->getName() . ':' . Crypt::hashBase64(serialize($libraries_to_load)) . (int) $optimize;
if ($cached = $this->cache->get($cid)) {
return $cached->data;
}
@ -132,7 +133,7 @@ class AssetResolver implements AssetResolverInterface {
'browsers' => [],
];
foreach ($this->getLibrariesToLoad($assets) as $library) {
foreach ($libraries_to_load as $library) {
list($extension, $name) = explode('/', $library, 2);
$definition = $this->libraryDiscovery->getLibraryByName($extension, $name);
if (isset($definition['css'])) {
@ -187,9 +188,7 @@ class AssetResolver implements AssetResolverInterface {
* Returns the JavaScript settings assets for this response's libraries.
*
* Gathers all drupalSettings from all libraries in the attached assets
* collection and merges them, then it merges individual attached settings,
* and finally invokes hook_js_settings_alter() to allow alterations of
* JavaScript settings by modules and themes.
* collection and merges them.
*
* @param \Drupal\Core\Asset\AttachedAssetsInterface $assets
* The assets attached to the current response.
@ -207,9 +206,6 @@ class AssetResolver implements AssetResolverInterface {
}
}
// Attached settings win over settings in libraries.
$settings = NestedArray::mergeDeepArray([$settings, $assets->getSettings()], TRUE);
return $settings;
}
@ -219,9 +215,10 @@ class AssetResolver implements AssetResolverInterface {
public function getJsAssets(AttachedAssetsInterface $assets, $optimize) {
$theme_info = $this->themeManager->getActiveTheme();
// Add the theme name to the cache key since themes may implement
// hook_js_alter(). Additionally add the current language to support
// translation of JavaScript files.
$cid = 'js:' . $theme_info->getName() . ':' . $this->languageManager->getCurrentLanguage()->getId() . ':' . Crypt::hashBase64(serialize($assets)) . (int) $optimize;
// hook_library_info_alter(). Additionally add the current language to
// support translation of JavaScript files via hook_js_alter().
$libraries_to_load = $this->getLibrariesToLoad($assets);
$cid = 'js:' . $theme_info->getName() . ':' . $this->languageManager->getCurrentLanguage()->getId() . ':' . Crypt::hashBase64(serialize($libraries_to_load)) . (int) (count($assets->getSettings()) > 0) . (int) $optimize;
if ($cached = $this->cache->get($cid)) {
list($js_assets_header, $js_assets_footer, $settings, $settings_in_header) = $cached->data;
@ -239,8 +236,6 @@ class AssetResolver implements AssetResolverInterface {
'browsers' => [],
];
$libraries_to_load = $this->getLibrariesToLoad($assets);
// Collect all libraries that contain JS assets and are in the header.
$header_js_libraries = [];
foreach ($libraries_to_load as $library) {
@ -329,8 +324,10 @@ class AssetResolver implements AssetResolverInterface {
$this->cache->set($cid, [$js_assets_header, $js_assets_footer, $settings, $settings_in_header], CacheBackendInterface::CACHE_PERMANENT, ['library_info']);
}
if ($settings !== FALSE) {
// Attached settings override both library definitions and
// hook_js_settings_build().
$settings = NestedArray::mergeDeepArray([$settings, $assets->getSettings()], TRUE);
// Allow modules and themes to alter the JavaScript settings.
$this->moduleHandler->alter('js_settings', $settings, $assets);
$this->themeManager->alter('js_settings', $settings, $assets);

View file

@ -7,6 +7,7 @@
namespace Drupal\Core\Cache;
use Drupal\Component\Utility\Crypt;
use Drupal\Core\DestructableInterface;
use Drupal\Core\Lock\LockBackendInterface;
@ -232,7 +233,7 @@ abstract class CacheCollector implements CacheCollectorInterface, DestructableIn
// Lock cache writes to help avoid stampedes.
$cid = $this->getCid();
$lock_name = $cid . ':' . __CLASS__;
$lock_name = $this->normalizeLockName($cid . ':' . __CLASS__);
if (!$lock || $this->lock->acquire($lock_name)) {
// Set and delete operations invalidate the cache item. Try to also load
// an eventually invalidated cache entry, only update an invalidated cache
@ -264,6 +265,30 @@ abstract class CacheCollector implements CacheCollectorInterface, DestructableIn
$this->keysToRemove = array();
}
/**
* Normalizes a cache ID in order to comply with database limitations.
*
* @param string $cid
* The passed in cache ID.
*
* @return string
* An ASCII-encoded cache ID that is at most 255 characters long.
*/
protected function normalizeLockName($cid) {
// Nothing to do if the ID is a US ASCII string of 255 characters or less.
$cid_is_ascii = mb_check_encoding($cid, 'ASCII');
if (strlen($cid) <= 255 && $cid_is_ascii) {
return $cid;
}
// Return a string that uses as much as possible of the original cache ID
// with the hash appended.
$hash = Crypt::hashBase64($cid);
if (!$cid_is_ascii) {
return $hash;
}
return substr($cid, 0, 255 - strlen($hash)) . $hash;
}
/**
* {@inheritdoc}
*/

View file

@ -10,6 +10,7 @@ namespace Drupal\Core\Composer;
use Drupal\Component\PhpStorage\FileStorage;
use Composer\Script\Event;
use Composer\Installer\PackageEvent;
use Composer\Semver\Constraint\Constraint;
/**
* Provides static functions for composer script events.
@ -71,23 +72,38 @@ class Composer {
];
/**
* Add vendor classes to composers static classmap.
* Add vendor classes to Composer's static classmap.
*/
public static function preAutoloadDump(Event $event) {
$composer = $event->getComposer();
$package = $composer->getPackage();
$autoload = $package->getAutoload();
$autoload['classmap'] = array_merge($autoload['classmap'], array(
'vendor/symfony/http-foundation/Request.php',
'vendor/symfony/http-foundation/ParameterBag.php',
'vendor/symfony/http-foundation/FileBag.php',
'vendor/symfony/http-foundation/ServerBag.php',
'vendor/symfony/http-foundation/HeaderBag.php',
'vendor/symfony/http-kernel/HttpKernel.php',
'vendor/symfony/http-kernel/HttpKernelInterface.php',
'vendor/symfony/http-kernel/TerminableInterface.php',
));
$package->setAutoload($autoload);
// We need the root package so we can add our classmaps to its loader.
$package = $event->getComposer()->getPackage();
// We need the local repository so that we can query and see if it's likely
// that our files are present there.
$repository = $event->getComposer()->getRepositoryManager()->getLocalRepository();
// This is, essentially, a null constraint. We only care whether the package
// is present in vendor/ yet, but findPackage() requires it.
$constraint = new Constraint('>', '');
// Check for our packages, and then optimize them if they're present.
if ($repository->findPackage('symfony/http-foundation', $constraint)) {
$autoload = $package->getAutoload();
$autoload['classmap'] = array_merge($autoload['classmap'], array(
'vendor/symfony/http-foundation/Request.php',
'vendor/symfony/http-foundation/ParameterBag.php',
'vendor/symfony/http-foundation/FileBag.php',
'vendor/symfony/http-foundation/ServerBag.php',
'vendor/symfony/http-foundation/HeaderBag.php',
));
$package->setAutoload($autoload);
}
if ($repository->findPackage('symfony/http-kernel', $constraint)) {
$autoload = $package->getAutoload();
$autoload['classmap'] = array_merge($autoload['classmap'], array(
'vendor/symfony/http-kernel/HttpKernel.php',
'vendor/symfony/http-kernel/HttpKernelInterface.php',
'vendor/symfony/http-kernel/TerminableInterface.php',
));
$package->setAutoload($autoload);
}
}
/**

View file

@ -112,13 +112,13 @@ class ConfigInstaller implements ConfigInstallerInterface {
$prefix = $name . '.';
}
// Gets a profile storage to search for overrides if necessary.
$profile_storage = $this->getProfileStorage($name);
// Gets profile storages to search for overrides if necessary.
$profile_storages = $this->getProfileStorages($name);
// Gather information about all the supported collections.
$collection_info = $this->configManager->getConfigCollectionInfo();
foreach ($collection_info->getCollectionNames() as $collection) {
$config_to_create = $this->getConfigToCreate($storage, $collection, $prefix, $profile_storage);
$config_to_create = $this->getConfigToCreate($storage, $collection, $prefix, $profile_storages);
// If we're installing a profile ensure configuration that is overriding
// is excluded.
if ($name == $this->drupalGetProfile()) {
@ -223,19 +223,22 @@ class ConfigInstaller implements ConfigInstallerInterface {
* The configuration collection to use.
* @param string $prefix
* (optional) Limit to configuration starting with the provided string.
* @param \Drupal\Core\Config\StorageInterface[] $profile_storages
* An array of storage interfaces containing profile configuration to check
* for overrides.
*
* @return array
* An array of configuration data read from the source storage keyed by the
* configuration object name.
*/
protected function getConfigToCreate(StorageInterface $storage, $collection, $prefix = '', StorageInterface $profile_storage = NULL) {
protected function getConfigToCreate(StorageInterface $storage, $collection, $prefix = '', array $profile_storages = []) {
if ($storage->getCollectionName() != $collection) {
$storage = $storage->createCollection($collection);
}
$data = $storage->readMultiple($storage->listAll($prefix));
// Check to see if the corresponding override storage has any overrides.
if ($profile_storage) {
foreach ($profile_storages as $profile_storage) {
if ($profile_storage->getCollectionName() != $collection) {
$profile_storage = $profile_storage->createCollection($collection);
}
@ -435,11 +438,11 @@ class ConfigInstaller implements ConfigInstallerInterface {
$enabled_extensions = $this->getEnabledExtensions();
// Add the extension that will be enabled to the list of enabled extensions.
$enabled_extensions[] = $name;
// Gets a profile storage to search for overrides if necessary.
$profile_storage = $this->getProfileStorage($name);
// Gets profile storages to search for overrides if necessary.
$profile_storages = $this->getProfileStorages($name);
// Check the dependencies of configuration provided by the module.
$invalid_default_config = $this->findDefaultConfigWithUnmetDependencies($storage, $enabled_extensions, $profile_storage);
$invalid_default_config = $this->findDefaultConfigWithUnmetDependencies($storage, $enabled_extensions, $profile_storages);
if (!empty($invalid_default_config)) {
throw UnmetDependenciesException::create($name, $invalid_default_config);
}
@ -460,14 +463,19 @@ class ConfigInstaller implements ConfigInstallerInterface {
/**
* Finds default configuration with unmet dependencies.
*
* @param \Drupal\Core\Config\StorageInterface $storage
* The storage containing the default configuration.
* @param array $enabled_extensions
* A list of all the currently enabled modules and themes.
* @param \Drupal\Core\Config\StorageInterface[] $profile_storages
* An array of storage interfaces containing profile configuration to check
* for overrides.
*
* @return array
* List of configuration that has unmet dependencies
*/
protected function findDefaultConfigWithUnmetDependencies(StorageInterface $storage, array $enabled_extensions, StorageInterface $profile_storage = NULL) {
$config_to_create = $this->getConfigToCreate($storage, StorageInterface::DEFAULT_COLLECTION, '', $profile_storage);
protected function findDefaultConfigWithUnmetDependencies(StorageInterface $storage, array $enabled_extensions, array $profile_storages = []) {
$config_to_create = $this->getConfigToCreate($storage, StorageInterface::DEFAULT_COLLECTION, '', $profile_storages);
$all_config = array_merge($this->configFactory->listAll(), array_keys($config_to_create));
return array_filter(array_keys($config_to_create), function($config_name) use ($enabled_extensions, $all_config, $config_to_create) {
return !$this->validateDependencies($config_name, $config_to_create[$config_name], $enabled_extensions, $all_config);
@ -550,27 +558,31 @@ class ConfigInstaller implements ConfigInstallerInterface {
/**
* Gets the profile storage to use to check for profile overrides.
*
* The install profile can override module configuration during a module
* install. Both the install and optional directories are checked for matching
* configuration. This allows profiles to override default configuration for
* modules they do not depend on.
*
* @param string $installing_name
* (optional) The name of the extension currently being installed.
*
* @return \Drupal\Core\Config\StorageInterface|null
* A storage to access configuration from the installation profile. If a
* Drupal installation is not in progress or we're installing the profile
* itself, then it will return NULL as the profile storage should not be
* used.
* @return \Drupal\Core\Config\StorageInterface[]|null
* Storages to access configuration from the installation profile. If we're
* installing the profile itself, then it will return an empty array as the
* profile storage should not be used.
*/
protected function getProfileStorage($installing_name = '') {
protected function getProfileStorages($installing_name = '') {
$profile = $this->drupalGetProfile();
if ($this->drupalInstallationAttempted() && $profile != $installing_name) {
// Profiles should not contain optional configuration so always use the
// install directory.
$profile_install_path = $this->getDefaultConfigDirectory('module', $profile);
$profile_storage = new FileStorage($profile_install_path, StorageInterface::DEFAULT_COLLECTION);
$profile_storages = [];
if ($profile && $profile != $installing_name) {
$profile_path = $this->drupalGetPath('module', $profile);
foreach ([InstallStorage::CONFIG_INSTALL_DIRECTORY, InstallStorage::CONFIG_OPTIONAL_DIRECTORY] as $directory) {
if (is_dir($profile_path . '/' . $directory)) {
$profile_storages[] = new FileStorage($profile_path . '/' . $directory, StorageInterface::DEFAULT_COLLECTION);
}
}
}
else {
$profile_storage = NULL;
}
return $profile_storage;
return $profile_storages;
}
/**

View file

@ -387,6 +387,7 @@ abstract class ConfigEntityBase extends Entity implements ConfigEntityInterface
* {@inheritdoc}
*/
public function url($rel = 'edit-form', $options = array()) {
// Do not remove this override: the default value of $rel is different.
return parent::url($rel, $options);
}
@ -394,9 +395,19 @@ abstract class ConfigEntityBase extends Entity implements ConfigEntityInterface
* {@inheritdoc}
*/
public function link($text = NULL, $rel = 'edit-form', array $options = []) {
// Do not remove this override: the default value of $rel is different.
return parent::link($text, $rel, $options);
}
/**
* {@inheritdoc}
*/
public function toUrl($rel = 'edit-form', array $options = []) {
// Unless language was already provided, avoid setting an explicit language.
$options += ['language' => NULL];
return parent::toUrl($rel, $options);
}
/**
* {@inheritdoc}
*/

View file

@ -49,6 +49,13 @@ abstract class ControllerBase implements ContainerInjectionInterface {
*/
protected $entityManager;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The entity form builder.
*
@ -117,6 +124,10 @@ abstract class ControllerBase implements ContainerInjectionInterface {
*
* @return \Drupal\Core\Entity\EntityManagerInterface
* The entity manager service.
*
* @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0.
* Most of the time static::entityTypeManager() is supposed to be used
* instead.
*/
protected function entityManager() {
if (!$this->entityManager) {
@ -125,6 +136,19 @@ abstract class ControllerBase implements ContainerInjectionInterface {
return $this->entityManager;
}
/**
* Retrieves the entity type manager.
*
* @return \Drupal\Core\Entity\EntityTypeManagerInterface
* The entity type manager.
*/
protected function entityTypeManager() {
if (!isset($this->entityTypeManager)) {
$this->entityTypeManager = $this->container()->get('entity_type.manager');
}
return $this->entityTypeManager;
}
/**
* Retrieves the entity form builder.
*

View file

@ -254,14 +254,24 @@ class DateHelper {
* An array of weekdays.
*
* @return array
* An array of weekdays reordered to match the first day of the week.
* An array of weekdays reordered to match the first day of the week. The
* keys will remain unchanged. For example, if the first day of the week is
* set to be Monday, the array keys will be [1, 2, 3, 4, 5, 6, 0].
*/
public static function weekDaysOrdered($weekdays) {
$first_day = \Drupal::config('system.date')->get('first_day');
if ($first_day > 0) {
for ($i = 1; $i <= $first_day; $i++) {
$last = array_shift($weekdays);
array_push($weekdays, $last);
// Reset the array to the first element.
reset($weekdays);
// Retrieve the first week day value.
$last = current($weekdays);
// Store the corresponding key.
$key = key($weekdays);
// Remove this week day from the beginning of the array.
unset($weekdays[$key]);
// Add this week day to the end of the array.
$weekdays[$key] = $last;
}
}
return $weekdays;

View file

@ -72,33 +72,6 @@ class ContainerBuilder extends SymfonyContainerBuilder {
parent::setParameter($name, $value);
}
/**
* Synchronizes a service change.
*
* This method is a copy of the ContainerBuilder of symfony.
*
* This method updates all services that depend on the given
* service by calling all methods referencing it.
*
* @param string $id A service id
*/
private function synchronize($id) {
foreach ($this->getDefinitions() as $definitionId => $definition) {
// only check initialized services
if (!$this->initialized($definitionId)) {
continue;
}
foreach ($definition->getMethodCalls() as $call) {
foreach ($call[1] as $argument) {
if ($argument instanceof Reference && $id == (string) $argument) {
$this->callMethod($this->get($definitionId), $call);
}
}
}
}
}
/**
* A 1to1 copy of parent::callMethod.
*/

View file

@ -821,13 +821,6 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
// If there is no container and no cached container definition, build a new
// one from scratch.
if (!isset($container) && !isset($container_definition)) {
if (version_compare(phpversion(), '7.0.0-dev') >= 0) {
// The service graph implementation is prone to corruption during GC.
// Collect cycles now then disable the GC for the time of the compiler
// run.
// @see https://bugs.php.net/bug.php?id=70805
gc_collect_cycles();
}
$container = $this->compileContainer();
// Only dump the container if dumping is allowed. This is useful for

View file

@ -10,10 +10,10 @@ namespace Drupal\Core\Entity\Element;
use Drupal\Component\Utility\Crypt;
use Drupal\Component\Utility\Tags;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityReferenceSelection\SelectionWithAutocreateInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element\Textfield;
use Drupal\Core\Site\Settings;
use Drupal\user\EntityOwnerInterface;
/**
* Provides an entity autocomplete form element.
@ -147,7 +147,7 @@ class EntityAutocomplete extends Textfield {
'handler_settings' => $element['#selection_settings'],
);
$handler = \Drupal::service('plugin.manager.entity_reference_selection')->getInstance($options);
$autocreate = (bool) $element['#autocreate'];
$autocreate = (bool) $element['#autocreate'] && $handler instanceof SelectionWithAutocreateInterface;
$input_values = $element['#tags'] ? Tags::explode($element['#value']) : array($element['#value']);
foreach ($input_values as $input) {
@ -167,13 +167,14 @@ class EntityAutocomplete extends Textfield {
// Auto-create item. See an example of how this is handled in
// \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem::presave().
$value[] = array(
'entity' => static::createNewEntity($element['#target_type'], $element['#autocreate']['bundle'], $input, $element['#autocreate']['uid'])
'entity' => $handler->createNewEntity($element['#target_type'], $element['#autocreate']['bundle'], $input, $element['#autocreate']['uid']),
);
}
}
// Check that the referenced entities are valid, if needed.
if ($element['#validate_reference'] && !$autocreate && !empty($value)) {
if ($element['#validate_reference'] && !empty($value)) {
// Validate existing entities.
$ids = array_reduce($value, function ($return, $item) {
if (isset($item['target_id'])) {
$return[] = $item['target_id'];
@ -189,6 +190,30 @@ class EntityAutocomplete extends Textfield {
}
}
}
// Validate newly created entities.
$new_entities = array_reduce($value, function ($return, $item) {
if (isset($item['entity'])) {
$return[] = $item['entity'];
}
return $return;
});
if ($new_entities) {
if ($autocreate) {
$valid_new_entities = $handler->validateReferenceableNewEntities($new_entities);
$invalid_new_entities = array_diff_key($new_entities, $valid_new_entities);
}
else {
// If the selection handler does not support referencing newly
// created entities, all of them should be invalidated.
$invalid_new_entities = $new_entities;
}
foreach ($invalid_new_entities as $entity) {
$form_state->setError($element, t('This entity (%type: %label) cannot be referenced.', array('%type' => $element['#target_type'], '%label' => $entity->label())));
}
}
}
// Use only the last value if the form element does not support multiple
@ -310,37 +335,4 @@ class EntityAutocomplete extends Textfield {
return $match;
}
/**
* Creates a new entity from a label entered in the autocomplete input.
*
* @param string $entity_type_id
* The entity type ID.
* @param string $bundle
* The bundle name.
* @param string $label
* The entity label.
* @param int $uid
* The entity owner ID.
*
* @return \Drupal\Core\Entity\EntityInterface
*/
protected static function createNewEntity($entity_type_id, $bundle, $label, $uid) {
$entity_manager = \Drupal::entityManager();
$entity_type = $entity_manager->getDefinition($entity_type_id);
$bundle_key = $entity_type->getKey('bundle');
$label_key = $entity_type->getKey('label');
$entity = $entity_manager->getStorage($entity_type_id)->create(array(
$bundle_key => $bundle,
$label_key => $label,
));
if ($entity instanceof EntityOwnerInterface) {
$entity->setOwnerId($uid);
}
return $entity;
}
}

View file

@ -72,11 +72,26 @@ abstract class Entity implements EntityInterface {
* Gets the entity manager.
*
* @return \Drupal\Core\Entity\EntityManagerInterface
*
* @deprecated in Drupal 8.0.0 and will be removed before Drupal 9.0.0.
* Use \Drupal::entityTypeManager() instead in most cases. If the needed
* method is not on \Drupal\Core\Entity\EntityTypeManagerInterface, see the
* deprecated \Drupal\Core\Entity\EntityManager to find the
* correct interface or service.
*/
protected function entityManager() {
return \Drupal::entityManager();
}
/**
* Gets the entity type manager.
*
* @return \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected function entityTypeManager() {
return \Drupal::entityTypeManager();
}
/**
* Gets the language manager.
*
@ -158,6 +173,13 @@ abstract class Entity implements EntityInterface {
* {@inheritdoc}
*/
public function urlInfo($rel = 'canonical', array $options = []) {
return $this->toUrl($rel, $options);
}
/**
* {@inheritdoc}
*/
public function toUrl($rel = 'canonical', array $options = []) {
if ($this->id() === NULL) {
throw new EntityMalformedException(sprintf('The "%s" entity cannot have a URI as it does not have an ID', $this->getEntityTypeId()));
}
@ -237,26 +259,33 @@ abstract class Entity implements EntityInterface {
* {@inheritdoc}
*/
public function link($text = NULL, $rel = 'canonical', array $options = []) {
if (is_null($text)) {
return $this->toLink($text, $rel, $options)->toString();
}
/**
* {@inheritdoc}
*/
public function toLink($text = NULL, $rel = 'canonical', array $options = []) {
if (!isset($text)) {
$text = $this->label();
}
$url = $this->urlInfo($rel);
$url = $this->toUrl($rel);
$options += $url->getOptions();
$url->setOptions($options);
return (new Link($text, $url))->toString();
return new Link($text, $url);
}
/**
* {@inheritdoc}
*/
public function url($rel = 'canonical', $options = array()) {
// While self::urlInfo() will throw an exception if the entity is new,
// While self::toUrl() will throw an exception if the entity has no id,
// the expected result for a URL is always a string.
if ($this->isNew() || !$this->hasLinkTemplate($rel)) {
if ($this->id() === NULL || !$this->hasLinkTemplate($rel)) {
return '';
}
$uri = $this->urlInfo($rel);
$uri = $this->toUrl($rel);
$options += $uri->getOptions();
$uri->setOptions($options);
return $uri->toString();

View file

@ -101,7 +101,29 @@ interface EntityInterface extends AccessibleInterface, CacheableDependencyInterf
public function label();
/**
* Gets the URI elements of the entity.
* Gets the URL object for the entity.
*
* @param string $rel
* The link relationship type, for example: canonical or edit-form.
* @param array $options
* See \Drupal\Core\Routing\UrlGeneratorInterface::generateFromRoute() for
* the available options.
*
* @return \Drupal\Core\Url
* The URL object.
*
* @deprecated in Drupal 8.0.0, intended to be removed in Drupal 9.0.0
* Use toUrl() instead.
*
* @see \Drupal\Core\Entity\EntityInterface::toUrl
*/
public function urlInfo($rel = 'canonical', array $options = array());
/**
* Gets the URL object for the entity.
*
* The entity must have an id already. Content entities usually get their IDs
* by saving them.
*
* URI templates might be set in the links array in an annotation, for
* example:
@ -128,8 +150,12 @@ interface EntityInterface extends AccessibleInterface, CacheableDependencyInterf
* the available options.
*
* @return \Drupal\Core\Url
* The URL object.
*
* @throws \Drupal\Core\Entity\EntityMalformedException
* @throws \Drupal\Core\Entity\Exception\UndefinedLinkTemplateException
*/
public function urlInfo($rel = 'canonical', array $options = array());
public function toUrl($rel = 'canonical', array $options = array());
/**
* Gets the public URL for this entity.
@ -142,9 +168,36 @@ interface EntityInterface extends AccessibleInterface, CacheableDependencyInterf
*
* @return string
* The URL for this entity.
*
* @deprecated in Drupal 8.0.0, intended to be removed in Drupal 9.0.0
* Please use toUrl() instead.
*
* @see \Drupal\Core\Entity\EntityInterface::toUrl
*/
public function url($rel = 'canonical', $options = array());
/**
* Deprecated way of generating a link to the entity. See toLink().
*
* @param string|null $text
* (optional) The link text for the anchor tag as a translated string.
* If NULL, it will use the entity's label. Defaults to NULL.
* @param string $rel
* (optional) The link relationship type. Defaults to 'canonical'.
* @param array $options
* See \Drupal\Core\Routing\UrlGeneratorInterface::generateFromRoute() for
* the available options.
*
* @return string
* An HTML string containing a link to the entity.
*
* @deprecated in Drupal 8.0.0, intended to be removed in Drupal 9.0.0
* Please use toLink() instead.
*
* @see \Drupal\Core\Entity\EntityInterface::toLink
*/
public function link($text = NULL, $rel = 'canonical', array $options = []);
/**
* Generates the HTML for a link to this entity.
*
@ -157,10 +210,13 @@ interface EntityInterface extends AccessibleInterface, CacheableDependencyInterf
* See \Drupal\Core\Routing\UrlGeneratorInterface::generateFromRoute() for
* the available options.
*
* @return string
* An HTML string containing a link to the entity.
* @return \Drupal\Core\Link
* A Link to the entity.
*
* @throws \Drupal\Core\Entity\EntityMalformedException
* @throws \Drupal\Core\Entity\Exception\UndefinedLinkTemplateException
*/
public function link($text = NULL, $rel = 'canonical', array $options = []);
public function toLink($text = NULL, $rel = 'canonical', array $options = []);
/**
* Indicates if a link template exists for a given key.

View file

@ -30,7 +30,7 @@ interface SelectionInterface extends PluginFormInterface {
public function getReferenceableEntities($match = NULL, $match_operator = 'CONTAINS', $limit = 0);
/**
* Counts entities that are referenceable by a given field.
* Counts entities that are referenceable.
*
* @return int
* The number of referenceable entities.
@ -38,7 +38,7 @@ interface SelectionInterface extends PluginFormInterface {
public function countReferenceableEntities($match = NULL, $match_operator = 'CONTAINS');
/**
* Validates that entities can be referenced by this field.
* Validates which existing entities can be referenced.
*
* @return array
* An array of valid entity IDs.

View file

@ -0,0 +1,52 @@
<?php
/**
* @file
* Contains \Drupal\Core\Entity\EntityReferenceSelection\SelectionWithAutocreateInterface.
*/
namespace Drupal\Core\Entity\EntityReferenceSelection;
/**
* Interface for Selection plugins that support newly created entities.
*
* @see \Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManager
* @see \Drupal\Core\Entity\Annotation\EntityReferenceSelection
* @see plugin_api
*/
interface SelectionWithAutocreateInterface {
/**
* Creates a new entity object that can be used as a valid reference.
*
* @param string $entity_type_id
* The entity type ID.
* @param string $bundle
* The bundle name.
* @param string $label
* The entity label.
* @param int $uid
* The entity owner ID, if the entity type supports it.
*
* @return \Drupal\Core\Entity\EntityInterface
* An unsaved entity object.
*/
public function createNewEntity($entity_type_id, $bundle, $label, $uid);
/**
* Validates which newly created entities can be referenced.
*
* This method should replicate the logic implemented by
* \Drupal\Core\Entity\EntityReferenceSelection\SelectionInterface::validateReferenceableEntities(),
* but applied to newly created entities that have not been saved yet.
*
* @param \Drupal\Core\Entity\EntityInterface[] $entities
* An array of entities to check.
*
* @return \Drupal\Core\Entity\EntityInterface[]
* The incoming $entities parameter, filtered for valid entities. Array keys
* are preserved.
*/
public function validateReferenceableNewEntities(array $entities);
}

View file

@ -97,18 +97,18 @@ class EntityTypeBundleInfo implements EntityTypeBundleInfoInterface {
}
else {
$this->bundleInfo = $this->moduleHandler->invokeAll('entity_bundle_info');
// First look for entity types that act as bundles for others, load them
// and add them as bundles.
foreach ($this->entityTypeManager->getDefinitions() as $type => $entity_type) {
if ($entity_type->getBundleOf()) {
foreach ($this->entityTypeManager->getStorage($type)->loadMultiple() as $entity) {
$this->bundleInfo[$entity_type->getBundleOf()][$entity->id()]['label'] = $entity->label();
// First look for entity types that act as bundles for others, load them
// and add them as bundles.
if ($bundle_entity_type = $entity_type->getBundleEntityType()) {
foreach ($this->entityTypeManager->getStorage($bundle_entity_type)->loadMultiple() as $entity) {
$this->bundleInfo[$type][$entity->id()]['label'] = $entity->label();
}
}
}
foreach ($this->entityTypeManager->getDefinitions() as $type => $entity_type) {
// If no bundles are provided, use the entity type name and label.
if (!isset($this->bundleInfo[$type])) {
// If entity type bundles are not supported and
// hook_entity_bundle_info() has not already set up bundle
// information, use the entity type name and label.
elseif (!isset($this->bundleInfo[$type])) {
$this->bundleInfo[$type][$type]['label'] = $entity_type->getLabel();
}
}

View file

@ -11,6 +11,7 @@ use Drupal\Component\Utility\Html;
use Drupal\Core\Database\Query\AlterableInterface;
use Drupal\Core\Database\Query\SelectInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\EntityReferenceSelection\SelectionWithAutocreateInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
use Drupal\Core\Form\FormStateInterface;
@ -18,6 +19,7 @@ use Drupal\Core\Entity\EntityReferenceSelection\SelectionInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Plugin\PluginBase;
use Drupal\Core\Session\AccountInterface;
use Drupal\user\EntityOwnerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
@ -40,7 +42,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
* deriver = "Drupal\Core\Entity\Plugin\Derivative\DefaultSelectionDeriver"
* )
*/
class DefaultSelection extends PluginBase implements SelectionInterface, ContainerFactoryPluginInterface {
class DefaultSelection extends PluginBase implements SelectionInterface, SelectionWithAutocreateInterface, ContainerFactoryPluginInterface {
/**
* The entity manager.
@ -288,6 +290,38 @@ class DefaultSelection extends PluginBase implements SelectionInterface, Contain
return $result;
}
/**
* {@inheritdoc}
*/
public function createNewEntity($entity_type_id, $bundle, $label, $uid) {
$entity_type = $this->entityManager->getDefinition($entity_type_id);
$bundle_key = $entity_type->getKey('bundle');
$label_key = $entity_type->getKey('label');
$entity = $this->entityManager->getStorage($entity_type_id)->create(array(
$bundle_key => $bundle,
$label_key => $label,
));
if ($entity instanceof EntityOwnerInterface) {
$entity->setOwnerId($uid);
}
return $entity;
}
/**
* {@inheritdoc}
*/
public function validateReferenceableNewEntities(array $entities) {
return array_filter($entities, function ($entity) {
if (isset($this->configuration['handler_settings']['target_bundles'])) {
return in_array($entity->bundle(), $this->configuration['handler_settings']['target_bundles']);
}
return TRUE;
});
}
/**
* Builds an EntityQuery to get referenceable entities.
*

View file

@ -26,10 +26,24 @@ class ValidReferenceConstraint extends Constraint {
*
* @var string
*/
public $message = 'The referenced entity (%type: %id) does not exist.';
public $message = 'This entity (%type: %id) cannot be referenced.';
/**
* Validation message when the target_id is empty.
* Violation message when the entity does not exist.
*
* @var string
*/
public $nonExistingMessage = 'The referenced entity (%type: %id) does not exist.';
/**
* Violation message when a new entity ("autocreate") is invalid.
*
* @var string
*/
public $invalidAutocreateMessage = 'This entity (%type: %label) cannot be referenced.';
/**
* Violation message when the target_id is empty.
*
* @var string
*/

View file

@ -7,39 +7,142 @@
namespace Drupal\Core\Entity\Plugin\Validation\Constraint;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManagerInterface;
use Drupal\Core\Entity\EntityReferenceSelection\SelectionWithAutocreateInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
/**
* Checks if referenced entities are valid.
*/
class ValidReferenceConstraintValidator extends ConstraintValidator {
class ValidReferenceConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {
/**
* The selection plugin manager.
*
* @var \Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManagerInterface
*/
protected $selectionManager;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs a ValidReferenceConstraintValidator object.
*
* @param \Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManagerInterface $selection_manager
* The selection plugin manager.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(SelectionPluginManagerInterface $selection_manager, EntityTypeManagerInterface $entity_type_manager) {
$this->selectionManager = $selection_manager;
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('plugin.manager.entity_reference_selection'),
$container->get('entity_type.manager')
);
}
/**
* {@inheritdoc}
*/
public function validate($value, Constraint $constraint) {
/** @var \Drupal\Core\Field\FieldItemInterface $value */
/** @var \Drupal\Core\Field\FieldItemListInterface $value */
/** @var ValidReferenceConstraint $constraint */
if (!isset($value)) {
return;
}
// We don't use a regular NotNull constraint for the target_id property as
// a NULL value is valid if the entity property contains an unsaved entity.
// @see \Drupal\Core\TypedData\DataReferenceTargetDefinition::getConstraints
if (!$value->isEmpty() && $value->target_id === NULL && !$value->entity->isNew()) {
$this->context->addViolation($constraint->nullMessage);
// Collect new entities and IDs of existing entities across the field items.
$new_entities = [];
$target_ids = [];
foreach ($value as $delta => $item) {
$target_id = $item->target_id;
// We don't use a regular NotNull constraint for the target_id property as
// NULL is allowed if the entity property contains an unsaved entity.
// @see \Drupal\Core\TypedData\DataReferenceTargetDefinition::getConstraints()
if (!$item->isEmpty() && $target_id === NULL) {
if (!$item->entity->isNew()) {
$this->context->buildViolation($constraint->nullMessage)
->atPath((string) $delta)
->addViolation();
return;
}
$new_entities[$delta] = $item->entity;
}
// '0' or NULL are considered valid empty references.
if (!empty($target_id)) {
$target_ids[$delta] = $target_id;
}
}
// Early opt-out if nothing to validate.
if (!$new_entities && !$target_ids) {
return;
}
$id = $value->get('target_id')->getValue();
// '0' or NULL are considered valid empty references.
if (empty($id)) {
return;
/** @var \Drupal\Core\Entity\EntityReferenceSelection\SelectionInterface $handler * */
$handler = $this->selectionManager->getSelectionHandler($value->getFieldDefinition());
$target_type_id = $value->getFieldDefinition()->getSetting('target_type');
// Add violations on deltas with a new entity that is not valid.
if ($new_entities) {
if ($handler instanceof SelectionWithAutocreateInterface) {
$valid_new_entities = $handler->validateReferenceableNewEntities($new_entities);
$invalid_new_entities = array_diff_key($new_entities, $valid_new_entities);
}
else {
// If the selection handler does not support referencing newly created
// entities, all of them should be invalidated.
$invalid_new_entities = $new_entities;
}
foreach ($invalid_new_entities as $delta => $entity) {
$this->context->buildViolation($constraint->invalidAutocreateMessage)
->setParameter('%type', $target_type_id)
->setParameter('%label', $entity->label())
->atPath((string) $delta . '.entity')
->setInvalidValue($entity)
->addViolation();
}
}
$referenced_entity = $value->get('entity')->getValue();
if (!$referenced_entity) {
$type = $value->getFieldDefinition()->getSetting('target_type');
$this->context->addViolation($constraint->message, array('%type' => $type, '%id' => $id));
// Add violations on deltas with a target_id that is not valid.
if ($target_ids) {
$valid_target_ids = $handler->validateReferenceableEntities($target_ids);
if ($invalid_target_ids = array_diff($target_ids, $valid_target_ids)) {
// For accuracy of the error message, differentiate non-referenceable
// and non-existent entities.
$target_type = $this->entityTypeManager->getDefinition($target_type_id);
$existing_ids = $this->entityTypeManager->getStorage($target_type_id)->getQuery()
->condition($target_type->getKey('id'), $invalid_target_ids, 'IN')
->execute();
foreach ($invalid_target_ids as $delta => $target_id) {
$message = in_array($target_id, $existing_ids) ? $constraint->message : $constraint->nonExistingMessage;
$this->context->buildViolation($message)
->setParameter('%type', $target_type_id)
->setParameter('%id', $target_id)
->atPath((string) $delta . '.target_id')
->setInvalidValue($target_id)
->addViolation();
}
}
}
}
}

View file

@ -7,7 +7,11 @@
namespace Drupal\Core\Entity\Routing;
use Drupal\Core\Entity\EntityHandlerInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
@ -24,7 +28,33 @@ use Symfony\Component\Routing\RouteCollection;
*
* @internal
*/
class DefaultHtmlRouteProvider implements EntityRouteProviderInterface {
class DefaultHtmlRouteProvider implements EntityRouteProviderInterface, EntityHandlerInterface {
/**
* The entity manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* Constructs a new DefaultHtmlRouteProvider.
*
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager.
*/
public function __construct(EntityManagerInterface $entity_manager) {
$this->entityManager = $entity_manager;
}
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static(
$container->get('entity.manager')
);
}
/**
* {@inheritdoc}
@ -71,6 +101,12 @@ class DefaultHtmlRouteProvider implements EntityRouteProviderInterface {
->setOption('parameters', [
$entity_type_id => ['type' => 'entity:' . $entity_type_id],
]);
// Entity types with serial IDs can specify this in their route
// requirements, improving the matching process.
if ($this->getEntityTypeIdKeyType($entity_type) === 'integer') {
$route->setRequirement($entity_type_id, '\d+');
}
return $route;
}
}
@ -102,6 +138,12 @@ class DefaultHtmlRouteProvider implements EntityRouteProviderInterface {
->setOption('parameters', [
$entity_type_id => ['type' => 'entity:' . $entity_type_id],
]);
// Entity types with serial IDs can specify this in their route
// requirements, improving the matching process.
if ($this->getEntityTypeIdKeyType($entity_type) === 'integer') {
$route->setRequirement($entity_type_id, '\d+');
}
return $route;
}
}
@ -128,8 +170,33 @@ class DefaultHtmlRouteProvider implements EntityRouteProviderInterface {
->setOption('parameters', [
$entity_type_id => ['type' => 'entity:' . $entity_type_id],
]);
// Entity types with serial IDs can specify this in their route
// requirements, improving the matching process.
if ($this->getEntityTypeIdKeyType($entity_type) === 'integer') {
$route->setRequirement($entity_type_id, '\d+');
}
return $route;
}
}
/**
* Gets the type of the ID key for a given entity type.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* An entity type.
*
* @return string|null
* The type of the ID key for a given entity type, or NULL if the entity
* type does not support fields.
*/
protected function getEntityTypeIdKeyType(EntityTypeInterface $entity_type) {
if (!$entity_type->isSubclassOf(FieldableEntityInterface::class)) {
return NULL;
}
$field_storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type->id());
return $field_storage_definitions[$entity_type->getKey('id')]->getType();
}
}

View file

@ -1916,16 +1916,16 @@ function hook_entity_extra_field_info() {
// Visibility of the ordering of the language selector is the same as on the
// node/add form.
if ($module_language_enabled) {
$configuration = ContentLanguageSettings::loadByEntityTypeBundle('node', $bundle->type);
$configuration = ContentLanguageSettings::loadByEntityTypeBundle('node', $bundle->id());
if ($configuration->isLanguageAlterable()) {
$extra['node'][$bundle->type]['form']['language'] = array(
$extra['node'][$bundle->id()]['form']['language'] = array(
'label' => t('Language'),
'description' => $description,
'weight' => 0,
);
}
}
$extra['node'][$bundle->type]['display']['language'] = array(
$extra['node'][$bundle->id()]['display']['language'] = array(
'label' => t('Language'),
'description' => $description,
'weight' => 0,
@ -1948,8 +1948,8 @@ function hook_entity_extra_field_info() {
function hook_entity_extra_field_info_alter(&$info) {
// Force node title to always be at the top of the list by default.
foreach (NodeType::loadMultiple() as $bundle) {
if (isset($info['node'][$bundle->type]['form']['title'])) {
$info['node'][$bundle->type]['form']['title']['weight'] = -20;
if (isset($info['node'][$bundle->id()]['form']['title'])) {
$info['node'][$bundle->id()]['form']['title']['weight'] = -20;
}
}
}

View file

@ -1,48 +0,0 @@
<?php
/**
* @file
* Contains \Drupal\Core\EventSubscriber\ContentControllerSubscriber.
*/
namespace Drupal\Core\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
/**
* Sets the request format onto the request object.
*
* @todo Remove this event subscriber after
* https://www.drupal.org/node/2092647 has landed.
*/
class ContentControllerSubscriber implements EventSubscriberInterface {
/**
* Sets the _controller on a request when a _form is defined.
*
* @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event
* The event to process.
*/
public function onRequestDeriveFormWrapper(GetResponseEvent $event) {
$request = $event->getRequest();
if ($request->attributes->has('_form')) {
$request->attributes->set('_controller', 'controller.form:getContentResult');
}
}
/**
* Registers the methods in this class that should be listeners.
*
* @return array
* An array of event listener definitions.
*/
static function getSubscribedEvents() {
$events[KernelEvents::REQUEST][] = array('onRequestDeriveFormWrapper', 25);
return $events;
}
}

View file

@ -147,7 +147,12 @@ class DefaultExceptionHtmlSubscriber extends HttpExceptionSubscriberBase {
}
$response = $this->httpKernel->handle($sub_request, HttpKernelInterface::SUB_REQUEST);
$response->setStatusCode($status_code);
// Only 2xx responses should have their status code overridden; any
// other status code should be passed on: redirects (3xx), error (5xx)…
// @see https://www.drupal.org/node/2603788#comment-10504916
if ($response->isSuccessful()) {
$response->setStatusCode($status_code);
}
// Persist any special HTTP headers that were set on the exception.
if ($exception instanceof HttpExceptionInterface) {

View file

@ -48,10 +48,6 @@ class HtmlResponsePlaceholderStrategySubscriber implements EventSubscriberInterf
* The event to process.
*/
public function onRespond(FilterResponseEvent $event) {
if (!$event->isMasterRequest()) {
return;
}
$response = $event->getResponse();
if (!$response instanceof HtmlResponse) {
return;

View file

@ -42,10 +42,6 @@ class HtmlResponseSubscriber implements EventSubscriberInterface {
* The event to process.
*/
public function onRespond(FilterResponseEvent $event) {
if (!$event->isMasterRequest()) {
return;
}
$response = $event->getResponse();
if (!$response instanceof HtmlResponse) {
return;

View file

@ -483,4 +483,18 @@ class ThemeHandler implements ThemeHandlerInterface {
throw new \InvalidArgumentException(sprintf('The theme %s does not exist.', $name));
}
/**
* {@inheritdoc}
*/
public function hasUi($name) {
$themes = $this->listInfo();
if (isset($themes[$name])) {
if (!empty($themes[$name]->info['hidden'])) {
$theme_config = $this->configFactory->get('system.theme');
return $name == $theme_config->get('default') || $name == $theme_config->get('admin');
}
return TRUE;
}
return FALSE;
}
}

View file

@ -208,4 +208,18 @@ interface ThemeHandlerInterface {
*/
public function getTheme($name);
/**
* Determines if a theme should be shown in the user interface.
*
* To be shown in the UI the theme has to be installed. If the theme is hidden
* it will not be shown unless it is the default or admin theme.
*
* @param string $name
* The name of the theme to check.
*
* @return bool
* TRUE if the theme should be shown in the UI, FALSE if not.
*/
public function hasUi($name);
}

View file

@ -15,6 +15,16 @@ use Drupal\Core\Form\FormStateInterface;
*/
class EntityReferenceFieldItemList extends FieldItemList implements EntityReferenceFieldItemListInterface {
/**
* {@inheritdoc}
*/
public function getConstraints() {
$constraints = parent::getConstraints();
$constraint_manager = $this->getTypedDataManager()->getValidationConstraintManager();
$constraints[] = $constraint_manager->create('ValidReference', []);
return $constraints;
}
/**
* {@inheritdoc}
*/

View file

@ -40,9 +40,6 @@ use Drupal\Core\Validation\Plugin\Validation\Constraint\AllowedValuesConstraint;
* default_widget = "entity_reference_autocomplete",
* default_formatter = "entity_reference_label",
* list_class = "\Drupal\Core\Field\EntityReferenceFieldItemList",
* default_widget = "entity_reference_autocomplete",
* default_formatter = "entity_reference_label",
* constraints = {"ValidReference" = {}}
* )
*/
class EntityReferenceItem extends FieldItemBase implements OptionsProviderInterface, PreconfiguredFieldUiOptionsInterface {
@ -165,20 +162,6 @@ class EntityReferenceItem extends FieldItemBase implements OptionsProviderInterf
unset($constraints[$key]);
}
}
list($current_handler) = explode(':', $this->getSetting('handler'), 2);
if ($current_handler === 'default') {
$handler_settings = $this->getSetting('handler_settings');
if (isset($handler_settings['target_bundles'])) {
$constraint_manager = \Drupal::typedDataManager()->getValidationConstraintManager();
$constraints[] = $constraint_manager->create('ComplexData', [
'entity' => [
'Bundle' => [
'bundle' => $handler_settings['target_bundles'],
],
],
]);
}
}
return $constraints;
}

View file

@ -7,6 +7,8 @@
namespace Drupal\Core\Field\Plugin\Field\FieldType;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\TypedData\DataDefinition;
@ -77,4 +79,16 @@ class UriItem extends StringItem {
return parent::isEmpty();
}
/**
* {@inheritdoc}
*/
public static function generateSampleValue(FieldDefinitionInterface $field_definition) {
$values = parent::generateSampleValue($field_definition);
$suffix_length = $field_definition->getSetting('max_length') - 7;
foreach ($values as $key => $value) {
$values[$key] = 'http://' . Unicode::substr($value, 0, $suffix_length);
}
return $values;
}
}

View file

@ -304,7 +304,9 @@ class LocalTaskManager extends DefaultPluginManager implements LocalTaskManagerI
}
// Pre-fetch all routes involved in the tree. This reduces the number
// of SQL queries that would otherwise be triggered by the access manager.
$routes = $route_names ? $this->routeProvider->getRoutesByNames($route_names) : array();
if ($route_names) {
$this->routeProvider->getRoutesByNames($route_names);
}
foreach ($tree as $level => $instances) {
/** @var $instances \Drupal\Core\Menu\LocalTaskInterface[] */

View file

@ -351,8 +351,11 @@ class MenuLinkManager implements MenuLinkManagerInterface {
* {@inheritdoc}
*/
public function addDefinition($id, array $definition) {
if ($this->treeStorage->load($id) || $id === '') {
throw new PluginException("The ID $id already exists as a plugin definition or is not valid");
if ($this->treeStorage->load($id)) {
throw new PluginException("The menu link ID $id already exists as a plugin definition");
}
elseif ($id === '') {
throw new PluginException("The menu link ID cannot be empty");
}
// Add defaults, so there is no requirement to specify everything.
$this->processDefinition($definition, $id);

View file

@ -177,6 +177,7 @@ class MenuLinkTree implements MenuLinkTreeInterface {
// Add the theme wrapper for outer markup.
// Allow menu-specific theme overrides.
$build['#theme'] = 'menu__' . strtr($menu_name, '-', '_');
$build['#menu_name'] = $menu_name;
$build['#items'] = $items;
// Set cache tag.
$build['#cache']['tags'][] = 'config:system.menu.' . $menu_name;

View file

@ -11,9 +11,14 @@ use Drupal\Core\Cache\Cache;
use Drupal\Core\Database\Connection;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Database\Query\Condition;
/**
* Provides a class for CRUD operations on path aliases.
*
* All queries perform case-insensitive matching on the 'source' and 'alias'
* fields, so the aliases '/test-alias' and '/test-Alias' are considered to be
* the same, and will both refer to the same internal system path.
*/
class AliasStorage implements AliasStorageInterface {
/**
@ -98,7 +103,13 @@ class AliasStorage implements AliasStorageInterface {
public function load($conditions) {
$select = $this->connection->select('url_alias');
foreach ($conditions as $field => $value) {
$select->condition($field, $value);
if ($field == 'source' || $field == 'alias') {
// Use LIKE for case-insensitive matching.
$select->condition($field, $this->connection->escapeLike($value), 'LIKE');
}
else {
$select->condition($field, $value);
}
}
return $select
->fields('url_alias')
@ -115,7 +126,13 @@ class AliasStorage implements AliasStorageInterface {
$path = $this->load($conditions);
$query = $this->connection->delete('url_alias');
foreach ($conditions as $field => $value) {
$query->condition($field, $value);
if ($field == 'source' || $field == 'alias') {
// Use LIKE for case-insensitive matching.
$query->condition($field, $this->connection->escapeLike($value), 'LIKE');
}
else {
$query->condition($field, $value);
}
}
$deleted = $query->execute();
// @todo Switch to using an event for this instead of a hook.
@ -128,90 +145,101 @@ class AliasStorage implements AliasStorageInterface {
* {@inheritdoc}
*/
public function preloadPathAlias($preloaded, $langcode) {
$args = array(
':system[]' => $preloaded,
':langcode' => $langcode,
':langcode_undetermined' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
);
$langcode_list = [$langcode, LanguageInterface::LANGCODE_NOT_SPECIFIED];
$select = $this->connection->select('url_alias')
->fields('url_alias', ['source', 'alias']);
if (!empty($preloaded)) {
$conditions = new Condition('OR');
foreach ($preloaded as $preloaded_item) {
$conditions->condition('source', $this->connection->escapeLike($preloaded_item), 'LIKE');
}
$select->condition($conditions);
}
// Always get the language-specific alias before the language-neutral one.
// For example 'de' is less than 'und' so the order needs to be ASC, while
// 'xx-lolspeak' is more than 'und' so the order needs to be DESC. We also
// order by pid ASC so that fetchAllKeyed() returns the most recently
// created alias for each source. Subsequent queries using fetchField() must
// use pid DESC to have the same effect. For performance reasons, the query
// builder is not used here.
// use pid DESC to have the same effect.
if ($langcode == LanguageInterface::LANGCODE_NOT_SPECIFIED) {
// Prevent PDO from complaining about a token the query doesn't use.
unset($args[':langcode']);
$result = $this->connection->query('SELECT source, alias FROM {url_alias} WHERE source IN ( :system[] ) AND langcode = :langcode_undetermined ORDER BY pid ASC', $args);
array_pop($langcode_list);
}
elseif ($langcode < LanguageInterface::LANGCODE_NOT_SPECIFIED) {
$result = $this->connection->query('SELECT source, alias FROM {url_alias} WHERE source IN ( :system[] ) AND langcode IN (:langcode, :langcode_undetermined) ORDER BY langcode ASC, pid ASC', $args);
$select->orderBy('langcode', 'ASC');
}
else {
$result = $this->connection->query('SELECT source, alias FROM {url_alias} WHERE source IN ( :system[] ) AND langcode IN (:langcode, :langcode_undetermined) ORDER BY langcode DESC, pid ASC', $args);
$select->orderBy('langcode', 'DESC');
}
return $result->fetchAllKeyed();
$select->orderBy('pid', 'ASC');
$select->condition('langcode', $langcode_list, 'IN');
return $select->execute()->fetchAllKeyed();
}
/**
* {@inheritdoc}
*/
public function lookupPathAlias($path, $langcode) {
$args = array(
':source' => $path,
':langcode' => $langcode,
':langcode_undetermined' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
);
// See the queries above.
$source = $this->connection->escapeLike($path);
$langcode_list = [$langcode, LanguageInterface::LANGCODE_NOT_SPECIFIED];
// See the queries above. Use LIKE for case-insensitive matching.
$select = $this->connection->select('url_alias')
->fields('url_alias', ['alias'])
->condition('source', $source, 'LIKE');
if ($langcode == LanguageInterface::LANGCODE_NOT_SPECIFIED) {
unset($args[':langcode']);
$alias = $this->connection->query("SELECT alias FROM {url_alias} WHERE source = :source AND langcode = :langcode_undetermined ORDER BY pid DESC", $args)->fetchField();
array_pop($langcode_list);
}
elseif ($langcode > LanguageInterface::LANGCODE_NOT_SPECIFIED) {
$alias = $this->connection->query("SELECT alias FROM {url_alias} WHERE source = :source AND langcode IN (:langcode, :langcode_undetermined) ORDER BY langcode DESC, pid DESC", $args)->fetchField();
$select->orderBy('langcode', 'DESC');
}
else {
$alias = $this->connection->query("SELECT alias FROM {url_alias} WHERE source = :source AND langcode IN (:langcode, :langcode_undetermined) ORDER BY langcode ASC, pid DESC", $args)->fetchField();
$select->orderBy('langcode', 'ASC');
}
return $alias;
$select->orderBy('pid', 'DESC');
$select->condition('langcode', $langcode_list, 'IN');
return $select->execute()->fetchField();
}
/**
* {@inheritdoc}
*/
public function lookupPathSource($path, $langcode) {
$args = array(
':alias' => $path,
':langcode' => $langcode,
':langcode_undetermined' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
);
// See the queries above.
$alias = $this->connection->escapeLike($path);
$langcode_list = [$langcode, LanguageInterface::LANGCODE_NOT_SPECIFIED];
// See the queries above. Use LIKE for case-insensitive matching.
$select = $this->connection->select('url_alias')
->fields('url_alias', ['source'])
->condition('alias', $alias, 'LIKE');
if ($langcode == LanguageInterface::LANGCODE_NOT_SPECIFIED) {
unset($args[':langcode']);
$result = $this->connection->query("SELECT source FROM {url_alias} WHERE alias = :alias AND langcode = :langcode_undetermined ORDER BY pid DESC", $args);
array_pop($langcode_list);
}
elseif ($langcode > LanguageInterface::LANGCODE_NOT_SPECIFIED) {
$result = $this->connection->query("SELECT source FROM {url_alias} WHERE alias = :alias AND langcode IN (:langcode, :langcode_undetermined) ORDER BY langcode DESC, pid DESC", $args);
$select->orderBy('langcode', 'DESC');
}
else {
$result = $this->connection->query("SELECT source FROM {url_alias} WHERE alias = :alias AND langcode IN (:langcode, :langcode_undetermined) ORDER BY langcode ASC, pid DESC", $args);
$select->orderBy('langcode', 'ASC');
}
return $result->fetchField();
$select->orderBy('pid', 'DESC');
$select->condition('langcode', $langcode_list, 'IN');
return $select->execute()->fetchField();
}
/**
* {@inheritdoc}
*/
public function aliasExists($alias, $langcode, $source = NULL) {
// Use LIKE and NOT LIKE for case-insensitive matching.
$query = $this->connection->select('url_alias')
->condition('alias', $alias)
->condition('alias', $this->connection->escapeLike($alias), 'LIKE')
->condition('langcode', $langcode);
if (!empty($source)) {
$query->condition('source', $source, '<>');
$query->condition('source', $this->connection->escapeLike($source), 'NOT LIKE');
}
$query->addExpression('1');
$query->range(0, 1);

View file

@ -44,6 +44,9 @@ interface AliasStorageInterface {
/**
* Fetches a specific URL alias from the database.
*
* The default implementation performs case-insensitive matching on the
* 'source' and 'alias' strings.
*
* @param array $conditions
* An array of query conditions.
*
@ -60,6 +63,9 @@ interface AliasStorageInterface {
/**
* Deletes a URL alias.
*
* The default implementation performs case-insensitive matching on the
* 'source' and 'alias' strings.
*
* @param array $conditions
* An array of criteria.
*/
@ -82,6 +88,9 @@ interface AliasStorageInterface {
/**
* Returns an alias of Drupal system URL.
*
* The default implementation performs case-insensitive matching on the
* 'source' and 'alias' strings.
*
* @param string $path
* The path to investigate for corresponding path aliases.
* @param string $langcode
@ -96,6 +105,9 @@ interface AliasStorageInterface {
/**
* Returns Drupal system URL of an alias.
*
* The default implementation performs case-insensitive matching on the
* 'source' and 'alias' strings.
*
* @param string $path
* The path to investigate for corresponding system URLs.
* @param string $langcode
@ -110,6 +122,9 @@ interface AliasStorageInterface {
/**
* Checks if alias already exists.
*
* The default implementation performs case-insensitive matching on the
* 'source' and 'alias' strings.
*
* @param string $alias
* Alias to check against.
* @param string $langcode
@ -135,8 +150,9 @@ interface AliasStorageInterface {
*
* @param array $header
* Table header.
* @param string[]|null $keys
* (optional) Search keys.
* @param string|null $keys
* (optional) Search keyword that may include one or more '*' as wildcard
* values.
*
* @return array
* Array of items to be displayed on the current page.

View file

@ -21,8 +21,27 @@ interface OutboundPathProcessorInterface {
* @param string $path
* The path to process, with a leading slash.
* @param array $options
* An array of options such as would be passed to the generator's
* generateFromRoute() method.
* (optional) An associative array of additional options, with the following
* elements:
* - 'query': An array of query key/value-pairs (without any URL-encoding)
* to append to the URL.
* - 'fragment': A fragment identifier (named anchor) to append to the URL.
* Do not include the leading '#' character.
* - 'absolute': Defaults to FALSE. Whether to force the output to be an
* absolute link (beginning with http:). Useful for links that will be
* displayed outside the site, such as in an RSS feed.
* - 'language': An optional language object used to look up the alias
* for the URL. If $options['language'] is omitted, it defaults to the
* current language for the language type LanguageInterface::TYPE_URL.
* - 'https': Whether this URL should point to a secure location. If not
* defined, the current scheme is used, so the user stays on HTTP or HTTPS
* respectively. TRUE enforces HTTPS and FALSE enforces HTTP.
* - 'base_url': Only used internally by a path processor, for example, to
* modify the base URL when a language dependent URL requires so.
* - 'prefix': Only used internally, to modify the path when a language
* dependent URL requires so.
* - 'route': The route object for the given path. It will be set by
* \Drupal\Core\Routing\UrlGenerator::generateFromRoute().
* @param \Symfony\Component\HttpFoundation\Request $request
* The HttpRequest object representing the current request.
* @param \Drupal\Core\Render\BubbleableMetadata $bubbleable_metadata

View file

@ -269,6 +269,11 @@ abstract class RenderElement extends PluginBase implements ElementInterface {
return $element;
}
// Add a data attribute to disable automatic refocus after ajax call.
if (!empty($element['#ajax']['disable-refocus'])) {
$element['#attributes']['data-disable-refocus'] = "true";
}
// Add a reasonable default event handler if none was specified.
if (isset($element['#ajax']) && !isset($element['#ajax']['event'])) {
switch ($element['#type']) {

View file

@ -0,0 +1,33 @@
<?php
/**
* @file
* Contains \Drupal\Core\Routing\Enhancer\FormRouteEnhancer.
*/
namespace Drupal\Core\Routing\Enhancer;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Route;
/**
* Enhancer to add a wrapping controller for _form routes.
*/
class FormRouteEnhancer implements RouteEnhancerInterface {
/**
* {@inheritdoc}
*/
public function applies(Route $route) {
return $route->hasDefault('_form') && !$route->hasDefault('_controller');
}
/**
* {@inheritdoc}
*/
public function enhance(array $defaults, Request $request) {
$defaults['_controller'] = 'controller.form:getContentResult';
return $defaults;
}
}

View file

@ -44,8 +44,12 @@ class ParamConversionEnhancer implements RouteEnhancerInterface, EventSubscriber
* {@inheritdoc}
*/
public function enhance(array $defaults, Request $request) {
$defaults['_raw_variables'] = $this->copyRawVariables($defaults);
return $this->paramConverterManager->convert($defaults);
// Just run the parameter conversion once per request.
if (!isset($defaults['_raw_variables'])) {
$defaults['_raw_variables'] = $this->copyRawVariables($defaults);
$defaults = $this->paramConverterManager->convert($defaults);
}
return $defaults;
}
/**

View file

@ -246,7 +246,7 @@ class RouteProvider implements PreloadableRouteProviderInterface, PagedRouteProv
* @return array
* An array of outlines that could match the specified path parts.
*/
public function getCandidateOutlines(array $parts) {
protected function getCandidateOutlines(array $parts) {
$number_parts = count($parts);
$ancestors = array();
$length = $number_parts - 1;
@ -355,7 +355,7 @@ class RouteProvider implements PreloadableRouteProviderInterface, PagedRouteProv
/**
* Comparison function for usort on routes.
*/
public function routeProviderRouteCompare(array $a, array $b) {
protected function routeProviderRouteCompare(array $a, array $b) {
if ($a['fit'] == $b['fit']) {
return strcmp($a['name'], $b['name']);
}

View file

@ -309,6 +309,9 @@ class UrlGenerator implements UrlGeneratorInterface {
$name = $this->getRouteDebugMessage($name);
$this->processRoute($name, $route, $parameters, $generated_url);
$path = $this->getInternalPathFromRoute($name, $route, $parameters, $query_params);
// Outbound path processors might need the route object for the path, e.g.
// to get the path pattern.
$options['route'] = $route;
$path = $this->processPath($path, $options, $generated_url);
if (!empty($options['prefix'])) {

View file

@ -77,6 +77,10 @@ interface UrlGeneratorInterface extends VersatileGeneratorInterface {
* @throws \Symfony\Component\Routing\Exception\InvalidParameterException
* Thrown when a parameter value for a placeholder is not correct because it
* does not match the requirement.
*
* @internal
* Should not be used in user code.
* Use \Drupal\Core\Url instead.
*/
public function generateFromRoute($name, $parameters = array(), $options = array(), $collect_bubbleable_metadata = FALSE);

View file

@ -47,28 +47,41 @@ class ReverseProxyMiddleware implements HttpKernelInterface {
*/
public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = TRUE) {
// Initialize proxy settings.
if ($this->settings->get('reverse_proxy', FALSE)) {
$ip_header = $this->settings->get('reverse_proxy_header', 'X_FORWARDED_FOR');
static::setSettingsOnRequest($request, $this->settings);
return $this->httpKernel->handle($request, $type, $catch);
}
/**
* Sets reverse proxy settings on Request object.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* A Request instance.
* @param \Drupal\Core\Site\Settings $settings
* The site settings.
*/
public static function setSettingsOnRequest(Request $request, Settings $settings) {
// Initialize proxy settings.
if ($settings->get('reverse_proxy', FALSE)) {
$ip_header = $settings->get('reverse_proxy_header', 'X_FORWARDED_FOR');
$request::setTrustedHeaderName($request::HEADER_CLIENT_IP, $ip_header);
$proto_header = $this->settings->get('reverse_proxy_proto_header', 'X_FORWARDED_PROTO');
$proto_header = $settings->get('reverse_proxy_proto_header', 'X_FORWARDED_PROTO');
$request::setTrustedHeaderName($request::HEADER_CLIENT_PROTO, $proto_header);
$host_header = $this->settings->get('reverse_proxy_host_header', 'X_FORWARDED_HOST');
$host_header = $settings->get('reverse_proxy_host_header', 'X_FORWARDED_HOST');
$request::setTrustedHeaderName($request::HEADER_CLIENT_HOST, $host_header);
$port_header = $this->settings->get('reverse_proxy_port_header', 'X_FORWARDED_PORT');
$port_header = $settings->get('reverse_proxy_port_header', 'X_FORWARDED_PORT');
$request::setTrustedHeaderName($request::HEADER_CLIENT_PORT, $port_header);
$forwarded_header = $this->settings->get('reverse_proxy_forwarded_header', 'FORWARDED');
$forwarded_header = $settings->get('reverse_proxy_forwarded_header', 'FORWARDED');
$request::setTrustedHeaderName($request::HEADER_FORWARDED, $forwarded_header);
$proxies = $this->settings->get('reverse_proxy_addresses', array());
$proxies = $settings->get('reverse_proxy_addresses', array());
if (count($proxies) > 0) {
$request::setTrustedProxies($proxies);
}
}
return $this->httpKernel->handle($request, $type, $catch);
}
}

View file

@ -134,6 +134,11 @@ class TranslationManager implements TranslationInterface, TranslatorInterface {
* The translated string.
*/
protected function doTranslate($string, array $options = array()) {
// If a NULL langcode has been provided, unset it.
if (!isset($options['langcode']) && array_key_exists('langcode', $options)) {
unset($options['langcode']);
}
// Merge in options defaults.
$options = $options + [
'langcode' => $this->defaultLangcode,

View file

@ -117,10 +117,11 @@ class Attribute implements \ArrayAccess, \IteratorAggregate, MarkupInterface {
* An AttributeValueBase representation of the attribute's value.
*/
protected function createAttributeValue($name, $value) {
// If the value is already an AttributeValueBase object, return it
// straight away.
// If the value is already an AttributeValueBase object,
// return a new instance of the same class, but with the new name.
if ($value instanceof AttributeValueBase) {
return $value;
$class = get_class($value);
return new $class($name, $value->value());
}
// An array value or 'class' attribute name are forced to always be an
// AttributeArray value for consistency.

View file

@ -0,0 +1,47 @@
<?php
/**
* @file
* Contains \Drupal\Core\Theme\MissingThemeDependencyException.
*/
namespace Drupal\Core\Theme;
/**
* Exception to be thrown when base theme for installed theme is not installed.
*
* @see \Drupal\Core\Theme\ThemeInitialization::getActiveThemeByName().
*/
class MissingThemeDependencyException extends \Exception {
/**
* The missing theme dependency.
*
* @var string
*/
protected $theme;
/**
* Constructs the exception.
*
* @param string $message
* The exception message.
* @param string $theme
* The missing theme dependency.
*/
public function __construct($message, $theme) {
parent::__construct($message);
$this->theme = $theme;
}
/**
* Gets the machine name of the missing theme.
*
* @return string
* The machine name of the theme that is missing.
*/
public function getMissingThemeName() {
return $this->theme;
}
}

View file

@ -109,6 +109,16 @@ class ThemeInitialization implements ThemeInitializationInterface {
$ancestor = $theme_name;
while ($ancestor && isset($themes[$ancestor]->base_theme)) {
$ancestor = $themes[$ancestor]->base_theme;
if (!$this->themeHandler->themeExists($ancestor)) {
if ($ancestor == 'stable') {
// Themes that depend on Stable will be fixed by system_update_8014().
// There is no harm in not adding it as an ancestor since at worst
// some people might experience slight visual regressions on
// update.php.
continue;
}
throw new MissingThemeDependencyException(sprintf('Base theme %s has not been installed.', $ancestor), $ancestor);
}
$base_themes[] = $themes[$ancestor];
}

View file

@ -34,6 +34,9 @@ interface ThemeInitializationInterface {
*
* @return \Drupal\Core\Theme\ActiveTheme
* An active theme object instance for the given theme.
*
* @throws \Drupal\Core\Theme\MissingThemeDependencyException
* Thrown when base theme for installed theme is not installed.
*/
public function getActiveThemeByName($theme_name);
@ -54,8 +57,8 @@ interface ThemeInitializationInterface {
* @param \Drupal\Core\Extension\Extension $theme
* The theme extension object.
* @param \Drupal\Core\Extension\Extension[] $base_themes
* An array of extension objects of base theme and its bases. It is ordered
* by 'oldest first', meaning the top level of the chain will be first.
* An array of extension objects of base theme and its bases. It is ordered
* by 'next parent first', meaning the top level of the chain will be first.
*
* @return \Drupal\Core\Theme\ActiveTheme
* The active theme instance for the passed in $theme.

View file

@ -72,6 +72,9 @@ interface LinkGeneratorInterface {
* @throws \Symfony\Component\Routing\Exception\InvalidParameterException
* Thrown when a parameter value for a placeholder is not correct because it
* does not match the requirement.
*
* @internal
* Should not be used in user code. Use \Drupal\Core\Link instead.
*/
public function generate($text, Url $url);
@ -84,6 +87,10 @@ interface LinkGeneratorInterface {
* @return \Drupal\Core\GeneratedLink
* A GeneratedLink object containing a link to the given route and
* parameters and bubbleable metadata.
*
* @internal
* Should not be used in user code.
* Use \Drupal\Core\Link instead.
*/
public function generateFromLink(Link $link);