summaryrefslogtreecommitdiffstats
path: root/core/lib/Drupal/Core
diff options
context:
space:
mode:
Diffstat (limited to 'core/lib/Drupal/Core')
-rw-r--r--core/lib/Drupal/Core/Access/AccessManagerInterface.php2
-rw-r--r--core/lib/Drupal/Core/Access/AccessResult.php27
-rw-r--r--core/lib/Drupal/Core/Access/AccessResultForbidden.php1
-rw-r--r--core/lib/Drupal/Core/Access/CheckProvider.php1
-rw-r--r--core/lib/Drupal/Core/Access/CheckProviderInterface.php1
-rw-r--r--core/lib/Drupal/Core/Access/CustomAccessCheck.php9
-rw-r--r--core/lib/Drupal/Core/Action/Plugin/Action/DeleteAction.php99
-rw-r--r--core/lib/Drupal/Core/Action/Plugin/Action/Derivative/EntityActionDeriverBase.php99
-rw-r--r--core/lib/Drupal/Core/Action/Plugin/Action/Derivative/EntityChangedActionDeriver.php22
-rw-r--r--core/lib/Drupal/Core/Action/Plugin/Action/Derivative/EntityDeleteActionDeriver.php40
-rw-r--r--core/lib/Drupal/Core/Action/Plugin/Action/Derivative/EntityPublishedActionDeriver.php23
-rw-r--r--core/lib/Drupal/Core/Action/Plugin/Action/EntityActionBase.php62
-rw-r--r--core/lib/Drupal/Core/Action/Plugin/Action/PublishAction.php38
-rw-r--r--core/lib/Drupal/Core/Action/Plugin/Action/SaveAction.php79
-rw-r--r--core/lib/Drupal/Core/Action/Plugin/Action/UnpublishAction.php38
-rw-r--r--core/lib/Drupal/Core/Ajax/AjaxFormHelperTrait.php56
-rw-r--r--core/lib/Drupal/Core/Ajax/AjaxHelperTrait.php39
-rw-r--r--core/lib/Drupal/Core/Ajax/OpenDialogCommand.php2
-rw-r--r--core/lib/Drupal/Core/Ajax/OpenModalDialogCommand.php1
-rw-r--r--core/lib/Drupal/Core/Ajax/OpenOffCanvasDialogCommand.php7
-rw-r--r--core/lib/Drupal/Core/Annotation/ContextDefinition.php28
-rw-r--r--core/lib/Drupal/Core/Archiver/ArchiveTar.php8
-rw-r--r--core/lib/Drupal/Core/Asset/CssOptimizer.php4
-rw-r--r--core/lib/Drupal/Core/Asset/JsOptimizer.php2
-rw-r--r--core/lib/Drupal/Core/Asset/LibraryDependencyResolver.php2
-rw-r--r--core/lib/Drupal/Core/Asset/LibraryDiscovery.php2
-rw-r--r--core/lib/Drupal/Core/Asset/LibraryDiscoveryParser.php6
-rw-r--r--core/lib/Drupal/Core/Batch/BatchBuilder.php340
-rw-r--r--core/lib/Drupal/Core/Block/BlockBase.php15
-rw-r--r--core/lib/Drupal/Core/Block/BlockManager.php34
-rw-r--r--core/lib/Drupal/Core/Block/BlockManagerInterface.php3
-rw-r--r--core/lib/Drupal/Core/Block/MessagesBlockPluginInterface.php3
-rw-r--r--core/lib/Drupal/Core/Block/Plugin/Block/Broken.php2
-rw-r--r--core/lib/Drupal/Core/Block/Plugin/Block/PageTitleBlock.php3
-rw-r--r--core/lib/Drupal/Core/Cache/ApcuBackend.php4
-rw-r--r--core/lib/Drupal/Core/Cache/Cache.php7
-rw-r--r--core/lib/Drupal/Core/Cache/CacheBackendInterface.php2
-rw-r--r--core/lib/Drupal/Core/Cache/CacheCollector.php3
-rw-r--r--core/lib/Drupal/Core/Cache/CacheTagsInvalidator.php3
-rw-r--r--core/lib/Drupal/Core/Cache/Context/CacheContextsManager.php2
-rw-r--r--core/lib/Drupal/Core/Cache/Context/LanguagesCacheContext.php2
-rw-r--r--core/lib/Drupal/Core/Cache/Context/SiteCacheContext.php2
-rw-r--r--core/lib/Drupal/Core/Cache/DatabaseBackend.php3
-rw-r--r--core/lib/Drupal/Core/Cache/DatabaseBackendFactory.php2
-rw-r--r--core/lib/Drupal/Core/Cache/MemoryBackend.php11
-rw-r--r--core/lib/Drupal/Core/Cache/MemoryCache/MemoryCache.php63
-rw-r--r--core/lib/Drupal/Core/Cache/MemoryCache/MemoryCacheInterface.php18
-rw-r--r--core/lib/Drupal/Core/Cache/PhpBackend.php4
-rw-r--r--core/lib/Drupal/Core/Command/DbDumpCommand.php17
-rw-r--r--core/lib/Drupal/Core/Command/InstallCommand.php337
-rw-r--r--core/lib/Drupal/Core/Command/QuickStartCommand.php76
-rw-r--r--core/lib/Drupal/Core/Command/ServerCommand.php278
-rw-r--r--core/lib/Drupal/Core/Composer/Composer.php50
-rw-r--r--core/lib/Drupal/Core/Condition/ConditionManager.php14
-rw-r--r--core/lib/Drupal/Core/Config/Config.php45
-rw-r--r--core/lib/Drupal/Core/Config/ConfigImporter.php16
-rw-r--r--core/lib/Drupal/Core/Config/ConfigInstaller.php44
-rw-r--r--core/lib/Drupal/Core/Config/ConfigInstallerInterface.php3
-rw-r--r--core/lib/Drupal/Core/Config/ConfigManager.php81
-rw-r--r--core/lib/Drupal/Core/Config/DatabaseStorage.php1
-rw-r--r--core/lib/Drupal/Core/Config/Development/ConfigSchemaChecker.php4
-rw-r--r--core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php15
-rw-r--r--core/lib/Drupal/Core/Config/Entity/ConfigEntityInterface.php2
-rw-r--r--core/lib/Drupal/Core/Config/Entity/ConfigEntityStorage.php77
-rw-r--r--core/lib/Drupal/Core/Config/Entity/ConfigEntityType.php50
-rw-r--r--core/lib/Drupal/Core/Config/Entity/ConfigEntityTypeInterface.php11
-rw-r--r--core/lib/Drupal/Core/Config/Entity/ConfigEntityUpdater.php119
-rw-r--r--core/lib/Drupal/Core/Config/Entity/DraggableListBuilder.php2
-rw-r--r--core/lib/Drupal/Core/Config/Entity/Query/Condition.php7
-rw-r--r--core/lib/Drupal/Core/Config/Entity/Query/QueryFactory.php4
-rw-r--r--core/lib/Drupal/Core/Config/Entity/ThirdPartySettingsInterface.php1
-rw-r--r--core/lib/Drupal/Core/Config/FileStorage.php8
-rw-r--r--core/lib/Drupal/Core/Config/Importer/ConfigImporterBatch.php77
-rw-r--r--core/lib/Drupal/Core/Config/PreExistingConfigException.php6
-rw-r--r--core/lib/Drupal/Core/Config/UnmetDependenciesException.php2
-rw-r--r--core/lib/Drupal/Core/Controller/ArgumentResolver/Psr7RequestValueResolver.php47
-rw-r--r--core/lib/Drupal/Core/Controller/ArgumentResolver/RawParameterValueResolver.php28
-rw-r--r--core/lib/Drupal/Core/Controller/ArgumentResolver/RouteMatchValueResolver.php30
-rw-r--r--core/lib/Drupal/Core/Controller/ControllerBase.php6
-rw-r--r--core/lib/Drupal/Core/Controller/ControllerResolver.php5
-rw-r--r--core/lib/Drupal/Core/Controller/FormController.php29
-rw-r--r--core/lib/Drupal/Core/Controller/HtmlFormController.php11
-rw-r--r--core/lib/Drupal/Core/Controller/TitleResolver.php15
-rw-r--r--core/lib/Drupal/Core/Database/Connection.php127
-rw-r--r--core/lib/Drupal/Core/Database/Database.php96
-rw-r--r--core/lib/Drupal/Core/Database/Driver/mysql/Connection.php328
-rw-r--r--core/lib/Drupal/Core/Database/Driver/mysql/Insert.php4
-rw-r--r--core/lib/Drupal/Core/Database/Driver/mysql/Install/Tasks.php14
-rw-r--r--core/lib/Drupal/Core/Database/Driver/mysql/Schema.php66
-rw-r--r--core/lib/Drupal/Core/Database/Driver/mysql/Upsert.php3
-rw-r--r--core/lib/Drupal/Core/Database/Driver/pgsql/Connection.php2
-rw-r--r--core/lib/Drupal/Core/Database/Driver/pgsql/Install/Tasks.php2
-rw-r--r--core/lib/Drupal/Core/Database/Driver/pgsql/Schema.php64
-rw-r--r--core/lib/Drupal/Core/Database/Driver/sqlite/Connection.php47
-rw-r--r--core/lib/Drupal/Core/Database/Driver/sqlite/Install/Tasks.php2
-rw-r--r--core/lib/Drupal/Core/Database/Driver/sqlite/Schema.php92
-rw-r--r--core/lib/Drupal/Core/Database/Driver/sqlite/Select.php1
-rw-r--r--core/lib/Drupal/Core/Database/Driver/sqlite/Statement.php2
-rw-r--r--core/lib/Drupal/Core/Database/Driver/sqlite/Truncate.php1
-rw-r--r--core/lib/Drupal/Core/Database/Install/Tasks.php4
-rw-r--r--core/lib/Drupal/Core/Database/Query/Condition.php3
-rw-r--r--core/lib/Drupal/Core/Database/Query/Merge.php2
-rw-r--r--core/lib/Drupal/Core/Database/Query/Query.php2
-rw-r--r--core/lib/Drupal/Core/Database/Query/Select.php11
-rw-r--r--core/lib/Drupal/Core/Database/Query/SelectExtender.php2
-rw-r--r--core/lib/Drupal/Core/Database/Query/SelectInterface.php8
-rw-r--r--core/lib/Drupal/Core/Database/Schema.php47
-rw-r--r--core/lib/Drupal/Core/Database/StatementInterface.php2
-rw-r--r--core/lib/Drupal/Core/Database/StatementPrefetch.php16
-rw-r--r--core/lib/Drupal/Core/Database/database.api.php294
-rw-r--r--core/lib/Drupal/Core/Datetime/DateHelper.php1
-rw-r--r--core/lib/Drupal/Core/Datetime/DrupalDateTime.php2
-rw-r--r--core/lib/Drupal/Core/Datetime/Element/Datetime.php14
-rw-r--r--core/lib/Drupal/Core/DependencyInjection/Compiler/CorsCompilerPass.php2
-rw-r--r--core/lib/Drupal/Core/DependencyInjection/Compiler/TwigExtensionPass.php4
-rw-r--r--core/lib/Drupal/Core/DependencyInjection/ContainerBuilder.php40
-rw-r--r--core/lib/Drupal/Core/DependencyInjection/DependencySerializationTrait.php32
-rw-r--r--core/lib/Drupal/Core/DependencyInjection/YamlFileLoader.php4
-rw-r--r--core/lib/Drupal/Core/Diff/DiffFormatter.php8
-rw-r--r--core/lib/Drupal/Core/DrupalKernel.php68
-rw-r--r--core/lib/Drupal/Core/Entity/Annotation/EntityReferenceSelection.php2
-rw-r--r--core/lib/Drupal/Core/Entity/Annotation/EntityType.php2
-rw-r--r--core/lib/Drupal/Core/Entity/BundleEntityFormBase.php2
-rw-r--r--core/lib/Drupal/Core/Entity/ContentEntityBase.php204
-rw-r--r--core/lib/Drupal/Core/Entity/ContentEntityDeleteForm.php5
-rw-r--r--core/lib/Drupal/Core/Entity/ContentEntityForm.php33
-rw-r--r--core/lib/Drupal/Core/Entity/ContentEntityInterface.php66
-rw-r--r--core/lib/Drupal/Core/Entity/ContentEntityNullStorage.php7
-rw-r--r--core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php357
-rw-r--r--core/lib/Drupal/Core/Entity/ContentEntityStorageInterface.php19
-rw-r--r--core/lib/Drupal/Core/Entity/ContentEntityType.php33
-rw-r--r--core/lib/Drupal/Core/Entity/Controller/EntityListController.php3
-rw-r--r--core/lib/Drupal/Core/Entity/Controller/EntityViewController.php3
-rw-r--r--core/lib/Drupal/Core/Entity/DynamicallyFieldableEntityStorageInterface.php25
-rw-r--r--core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php6
-rw-r--r--core/lib/Drupal/Core/Entity/Enhancer/EntityRouteEnhancer.php1
-rw-r--r--core/lib/Drupal/Core/Entity/Entity.php7
-rw-r--r--core/lib/Drupal/Core/Entity/Entity/EntityFormDisplay.php4
-rw-r--r--core/lib/Drupal/Core/Entity/Entity/EntityViewDisplay.php11
-rw-r--r--core/lib/Drupal/Core/Entity/EntityAccessControlHandler.php14
-rw-r--r--core/lib/Drupal/Core/Entity/EntityBundleListener.php1
-rw-r--r--core/lib/Drupal/Core/Entity/EntityChangedInterface.php5
-rw-r--r--core/lib/Drupal/Core/Entity/EntityChangesDetectionTrait.php43
-rw-r--r--core/lib/Drupal/Core/Entity/EntityCreateAccessCheck.php2
-rw-r--r--core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManager.php41
-rw-r--r--core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManagerInterface.php12
-rw-r--r--core/lib/Drupal/Core/Entity/EntityDeleteFormTrait.php2
-rw-r--r--core/lib/Drupal/Core/Entity/EntityDeleteMultipleAccessCheck.php86
-rw-r--r--core/lib/Drupal/Core/Entity/EntityDisplayBase.php18
-rw-r--r--core/lib/Drupal/Core/Entity/EntityFieldManager.php21
-rw-r--r--core/lib/Drupal/Core/Entity/EntityForm.php11
-rw-r--r--core/lib/Drupal/Core/Entity/EntityLastInstalledSchemaRepository.php22
-rw-r--r--core/lib/Drupal/Core/Entity/EntityLastInstalledSchemaRepositoryInterface.php28
-rw-r--r--core/lib/Drupal/Core/Entity/EntityListBuilder.php4
-rw-r--r--core/lib/Drupal/Core/Entity/EntityListBuilderInterface.php3
-rw-r--r--core/lib/Drupal/Core/Entity/EntityManager.php6
-rw-r--r--core/lib/Drupal/Core/Entity/EntityPublishedTrait.php3
-rw-r--r--core/lib/Drupal/Core/Entity/EntityRepository.php4
-rw-r--r--core/lib/Drupal/Core/Entity/EntityStorageBase.php65
-rw-r--r--core/lib/Drupal/Core/Entity/EntityStorageInterface.php12
-rw-r--r--core/lib/Drupal/Core/Entity/EntityType.php41
-rw-r--r--core/lib/Drupal/Core/Entity/EntityTypeInterface.php43
-rw-r--r--core/lib/Drupal/Core/Entity/EntityTypeManager.php1
-rw-r--r--core/lib/Drupal/Core/Entity/EntityTypeManagerInterface.php3
-rw-r--r--core/lib/Drupal/Core/Entity/EntityViewBuilder.php45
-rw-r--r--core/lib/Drupal/Core/Entity/FieldableEntityStorageInterface.php26
-rw-r--r--core/lib/Drupal/Core/Entity/Form/DeleteMultipleForm.php323
-rw-r--r--core/lib/Drupal/Core/Entity/HtmlEntityFormController.php10
-rw-r--r--core/lib/Drupal/Core/Entity/KeyValueStore/KeyValueContentEntityStorage.php37
-rw-r--r--core/lib/Drupal/Core/Entity/KeyValueStore/KeyValueEntityStorage.php10
-rw-r--r--core/lib/Drupal/Core/Entity/Plugin/DataType/ConfigEntityAdapter.php146
-rw-r--r--core/lib/Drupal/Core/Entity/Plugin/DataType/Deriver/EntityDeriver.php9
-rw-r--r--core/lib/Drupal/Core/Entity/Plugin/DataType/EntityAdapter.php8
-rw-r--r--core/lib/Drupal/Core/Entity/Plugin/DataType/EntityReference.php2
-rw-r--r--core/lib/Drupal/Core/Entity/Plugin/EntityReferenceSelection/DefaultSelection.php2
-rw-r--r--core/lib/Drupal/Core/Entity/Plugin/EntityReferenceSelection/PhpSelection.php9
-rw-r--r--core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityChangedConstraintValidator.php21
-rw-r--r--core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityHasFieldConstraint.php53
-rw-r--r--core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityHasFieldConstraintValidator.php35
-rw-r--r--core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityUntranslatableFieldsConstraint.php21
-rw-r--r--core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityUntranslatableFieldsConstraintValidator.php123
-rw-r--r--core/lib/Drupal/Core/Entity/Query/ConditionAggregateInterface.php2
-rw-r--r--core/lib/Drupal/Core/Entity/Query/ConditionFundamentals.php2
-rw-r--r--core/lib/Drupal/Core/Entity/Query/ConditionInterface.php5
-rw-r--r--core/lib/Drupal/Core/Entity/Query/QueryBase.php2
-rw-r--r--core/lib/Drupal/Core/Entity/Query/QueryFactory.php7
-rw-r--r--core/lib/Drupal/Core/Entity/Query/Sql/Condition.php10
-rw-r--r--core/lib/Drupal/Core/Entity/Query/Sql/Query.php1
-rw-r--r--core/lib/Drupal/Core/Entity/Query/Sql/QueryAggregate.php2
-rw-r--r--core/lib/Drupal/Core/Entity/Query/Sql/Tables.php39
-rw-r--r--core/lib/Drupal/Core/Entity/RevisionableInterface.php52
-rw-r--r--core/lib/Drupal/Core/Entity/RevisionableStorageInterface.php68
-rw-r--r--core/lib/Drupal/Core/Entity/Routing/AdminHtmlRouteProvider.php10
-rw-r--r--core/lib/Drupal/Core/Entity/Routing/DefaultHtmlRouteProvider.php26
-rw-r--r--core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php247
-rw-r--r--core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php364
-rw-r--r--core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php300
-rw-r--r--core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchemaConverter.php26
-rw-r--r--core/lib/Drupal/Core/Entity/TranslatableInterface.php20
-rw-r--r--core/lib/Drupal/Core/Entity/TranslatableRevisionableInterface.php83
-rw-r--r--core/lib/Drupal/Core/Entity/TranslatableRevisionableStorageInterface.php46
-rw-r--r--core/lib/Drupal/Core/Entity/TranslatableStorageInterface.php30
-rw-r--r--core/lib/Drupal/Core/Entity/TypedData/EntityDataDefinition.php43
-rw-r--r--core/lib/Drupal/Core/Entity/entity.api.php222
-rw-r--r--core/lib/Drupal/Core/EventSubscriber/ActiveLinkResponseFilter.php4
-rw-r--r--core/lib/Drupal/Core/EventSubscriber/ConfigImportSubscriber.php38
-rw-r--r--core/lib/Drupal/Core/EventSubscriber/ConfigSnapshotSubscriber.php4
-rw-r--r--core/lib/Drupal/Core/EventSubscriber/EarlyRenderingControllerWrapperSubscriber.php18
-rw-r--r--core/lib/Drupal/Core/EventSubscriber/EnforcedFormResponseSubscriber.php1
-rw-r--r--core/lib/Drupal/Core/EventSubscriber/Fast404ExceptionHtmlSubscriber.php1
-rw-r--r--core/lib/Drupal/Core/EventSubscriber/FinalExceptionSubscriber.php12
-rw-r--r--core/lib/Drupal/Core/EventSubscriber/KernelDestructionSubscriber.php5
-rw-r--r--core/lib/Drupal/Core/EventSubscriber/MaintenanceModeSubscriber.php4
-rw-r--r--core/lib/Drupal/Core/EventSubscriber/OptionsRequestSubscriber.php8
-rw-r--r--core/lib/Drupal/Core/EventSubscriber/RedirectResponseSubscriber.php32
-rw-r--r--core/lib/Drupal/Core/Executable/ExecutablePluginBase.php10
-rw-r--r--core/lib/Drupal/Core/Extension/Exception/UninstalledExtensionException.php8
-rw-r--r--core/lib/Drupal/Core/Extension/Exception/UnknownExtensionException.php8
-rw-r--r--core/lib/Drupal/Core/Extension/ExtensionDiscovery.php2
-rw-r--r--core/lib/Drupal/Core/Extension/ExtensionList.php544
-rw-r--r--core/lib/Drupal/Core/Extension/InfoParserDynamic.php18
-rw-r--r--core/lib/Drupal/Core/Extension/InfoParserInterface.php5
-rw-r--r--core/lib/Drupal/Core/Extension/ModuleExtensionList.php228
-rw-r--r--core/lib/Drupal/Core/Extension/ModuleHandler.php65
-rw-r--r--core/lib/Drupal/Core/Extension/ModuleHandlerInterface.php91
-rw-r--r--core/lib/Drupal/Core/Extension/ModuleInstaller.php55
-rw-r--r--core/lib/Drupal/Core/Extension/ProfileExtensionList.php29
-rw-r--r--core/lib/Drupal/Core/Extension/ThemeHandler.php8
-rw-r--r--core/lib/Drupal/Core/Extension/ThemeHandlerInterface.php10
-rw-r--r--core/lib/Drupal/Core/Extension/ThemeInstaller.php9
-rw-r--r--core/lib/Drupal/Core/Extension/ThemeInstallerInterface.php8
-rw-r--r--core/lib/Drupal/Core/Extension/module.api.php25
-rw-r--r--core/lib/Drupal/Core/Field/BaseFieldDefinition.php105
-rw-r--r--core/lib/Drupal/Core/Field/BaseFieldOverrideStorage.php10
-rw-r--r--core/lib/Drupal/Core/Field/ChangedFieldItemList.php12
-rw-r--r--core/lib/Drupal/Core/Field/DeletedFieldsRepository.php97
-rw-r--r--core/lib/Drupal/Core/Field/DeletedFieldsRepositoryInterface.php73
-rw-r--r--core/lib/Drupal/Core/Field/Entity/BaseFieldOverride.php7
-rw-r--r--core/lib/Drupal/Core/Field/EntityReferenceFieldItemList.php2
-rw-r--r--core/lib/Drupal/Core/Field/FieldConfigBase.php17
-rw-r--r--core/lib/Drupal/Core/Field/FieldDefinitionInterface.php7
-rw-r--r--core/lib/Drupal/Core/Field/FieldInputValueNormalizerTrait.php42
-rw-r--r--core/lib/Drupal/Core/Field/FieldItemInterface.php4
-rw-r--r--core/lib/Drupal/Core/Field/FieldItemList.php29
-rw-r--r--core/lib/Drupal/Core/Field/FieldItemListInterface.php25
-rw-r--r--core/lib/Drupal/Core/Field/FieldModuleUninstallValidator.php66
-rw-r--r--core/lib/Drupal/Core/Field/FieldStorageDefinitionInterface.php16
-rw-r--r--core/lib/Drupal/Core/Field/FieldStorageDefinitionListener.php31
-rw-r--r--core/lib/Drupal/Core/Field/FieldTypePluginManager.php15
-rw-r--r--core/lib/Drupal/Core/Field/FieldTypePluginManagerInterface.php20
-rw-r--r--core/lib/Drupal/Core/Field/MapFieldItemList.php34
-rw-r--r--core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/EntityReferenceEntityFormatter.php4
-rw-r--r--core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/LanguageFormatter.php14
-rw-r--r--core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/StringFormatter.php31
-rw-r--r--core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/TimestampAgoFormatter.php30
-rw-r--r--core/lib/Drupal/Core/Field/Plugin/Field/FieldType/DecimalItem.php6
-rw-r--r--core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EmailItem.php2
-rw-r--r--core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItem.php69
-rw-r--r--core/lib/Drupal/Core/Field/Plugin/Field/FieldType/LanguageItem.php68
-rw-r--r--core/lib/Drupal/Core/Field/Plugin/Field/FieldType/MapItem.php3
-rw-r--r--core/lib/Drupal/Core/Field/Plugin/Field/FieldType/NumericItemBase.php4
-rw-r--r--core/lib/Drupal/Core/Field/Plugin/Field/FieldType/TimestampItem.php1
-rw-r--r--core/lib/Drupal/Core/Field/Plugin/Field/FieldType/UriItem.php3
-rw-r--r--core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/BooleanCheckboxWidget.php2
-rw-r--r--core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/EntityReferenceAutocompleteWidget.php4
-rw-r--r--core/lib/Drupal/Core/Field/Plugin/migrate/field/Email.php10
-rw-r--r--core/lib/Drupal/Core/Field/Plugin/migrate/field/d7/EntityReference.php15
-rw-r--r--core/lib/Drupal/Core/Field/PluginSettingsBase.php2
-rw-r--r--core/lib/Drupal/Core/Field/PreconfiguredFieldUiOptionsInterface.php6
-rw-r--r--core/lib/Drupal/Core/Field/WidgetBase.php37
-rw-r--r--core/lib/Drupal/Core/Field/WidgetPluginManager.php1
-rw-r--r--core/lib/Drupal/Core/File/MimeType/MimeTypeGuesser.php2
-rw-r--r--core/lib/Drupal/Core/FileTransfer/FileTransfer.php3
-rw-r--r--core/lib/Drupal/Core/FileTransfer/Form/FileTransferAuthorizeForm.php2
-rw-r--r--core/lib/Drupal/Core/Form/ConfigFormBase.php2
-rw-r--r--core/lib/Drupal/Core/Form/ConfirmFormInterface.php8
-rw-r--r--core/lib/Drupal/Core/Form/EnforcedResponse.php2
-rw-r--r--core/lib/Drupal/Core/Form/EventSubscriber/FormAjaxSubscriber.php35
-rw-r--r--core/lib/Drupal/Core/Form/FormBase.php2
-rw-r--r--core/lib/Drupal/Core/Form/FormBuilder.php12
-rw-r--r--core/lib/Drupal/Core/Form/FormErrorHandler.php17
-rw-r--r--core/lib/Drupal/Core/Form/FormInterface.php4
-rw-r--r--core/lib/Drupal/Core/Form/FormState.php10
-rw-r--r--core/lib/Drupal/Core/Form/FormStateInterface.php5
-rw-r--r--core/lib/Drupal/Core/Form/FormValidator.php15
-rw-r--r--core/lib/Drupal/Core/Form/form.api.php6
-rw-r--r--core/lib/Drupal/Core/GeneratedLink.php5
-rw-r--r--core/lib/Drupal/Core/GeneratedUrl.php2
-rw-r--r--core/lib/Drupal/Core/Http/ClientFactory.php2
-rw-r--r--core/lib/Drupal/Core/Http/HandlerStackConfigurator.php2
-rw-r--r--core/lib/Drupal/Core/Http/TrustedHostsRequestFactory.php2
-rw-r--r--core/lib/Drupal/Core/ImageToolkit/ImageToolkitBase.php1
-rw-r--r--core/lib/Drupal/Core/ImageToolkit/ImageToolkitOperationManager.php4
-rw-r--r--core/lib/Drupal/Core/Installer/ConfigOverride.php62
-rw-r--r--core/lib/Drupal/Core/Installer/Exception/InstallProfileMismatchException.php4
-rw-r--r--core/lib/Drupal/Core/Installer/Form/SelectProfileForm.php82
-rw-r--r--core/lib/Drupal/Core/Installer/Form/SiteConfigureForm.php37
-rw-r--r--core/lib/Drupal/Core/Installer/Form/SiteSettingsForm.php7
-rw-r--r--core/lib/Drupal/Core/Installer/InstallerModuleExtensionList.php58
-rw-r--r--core/lib/Drupal/Core/Installer/InstallerServiceProvider.php3
-rw-r--r--core/lib/Drupal/Core/Installer/NormalInstallerServiceProvider.php21
-rw-r--r--core/lib/Drupal/Core/KeyValueStore/DatabaseStorageExpirable.php2
-rw-r--r--core/lib/Drupal/Core/KeyValueStore/KeyValueFactory.php2
-rw-r--r--core/lib/Drupal/Core/Language/LanguageManager.php7
-rw-r--r--core/lib/Drupal/Core/Layout/LayoutPluginManager.php29
-rw-r--r--core/lib/Drupal/Core/Layout/LayoutPluginManagerInterface.php3
-rw-r--r--core/lib/Drupal/Core/Lock/DatabaseLockBackend.php6
-rw-r--r--core/lib/Drupal/Core/Logger/LogMessageParser.php4
-rw-r--r--core/lib/Drupal/Core/Logger/LogMessageParserInterface.php7
-rw-r--r--core/lib/Drupal/Core/Logger/RfcLogLevel.php4
-rw-r--r--core/lib/Drupal/Core/Mail/MailFormatHelper.php7
-rw-r--r--core/lib/Drupal/Core/Mail/MailManager.php18
-rw-r--r--core/lib/Drupal/Core/Mail/Plugin/Mail/PhpMail.php48
-rw-r--r--core/lib/Drupal/Core/Menu/InaccessibleMenuLink.php1
-rw-r--r--core/lib/Drupal/Core/Menu/LocalActionDefault.php27
-rw-r--r--core/lib/Drupal/Core/Menu/LocalActionManager.php38
-rw-r--r--core/lib/Drupal/Core/Menu/LocalTaskDefault.php28
-rw-r--r--core/lib/Drupal/Core/Menu/LocalTaskManager.php32
-rw-r--r--core/lib/Drupal/Core/Menu/MenuLinkManager.php1
-rw-r--r--core/lib/Drupal/Core/Menu/MenuLinkTreeInterface.php2
-rw-r--r--core/lib/Drupal/Core/Menu/Plugin/Block/LocalTasksBlock.php1
-rw-r--r--core/lib/Drupal/Core/Menu/menu.api.php10
-rw-r--r--core/lib/Drupal/Core/Messenger/LegacyMessenger.php269
-rw-r--r--core/lib/Drupal/Core/Messenger/Messenger.php112
-rw-r--r--core/lib/Drupal/Core/Messenger/MessengerInterface.php9
-rw-r--r--core/lib/Drupal/Core/Messenger/MessengerTrait.php40
-rw-r--r--core/lib/Drupal/Core/PageCache/RequestPolicy/NoSessionOpen.php2
-rw-r--r--core/lib/Drupal/Core/ParamConverter/EntityConverter.php154
-rw-r--r--core/lib/Drupal/Core/Password/PhpassHashedPassword.php2
-rw-r--r--core/lib/Drupal/Core/Path/AliasManager.php2
-rw-r--r--core/lib/Drupal/Core/Path/AliasStorage.php2
-rw-r--r--core/lib/Drupal/Core/PathProcessor/InboundPathProcessorInterface.php9
-rw-r--r--core/lib/Drupal/Core/PathProcessor/PathProcessorAlias.php9
-rw-r--r--core/lib/Drupal/Core/PathProcessor/PathProcessorManager.php1
-rw-r--r--core/lib/Drupal/Core/Plugin/Context/Context.php3
-rw-r--r--core/lib/Drupal/Core/Plugin/Context/ContextAwarePluginManagerInterface.php2
-rw-r--r--core/lib/Drupal/Core/Plugin/Context/ContextDefinition.php188
-rw-r--r--core/lib/Drupal/Core/Plugin/Context/ContextDefinitionInterface.php12
-rw-r--r--core/lib/Drupal/Core/Plugin/Context/ContextHandler.php65
-rw-r--r--core/lib/Drupal/Core/Plugin/Context/ContextHandlerInterface.php3
-rw-r--r--core/lib/Drupal/Core/Plugin/Context/ContextProviderInterface.php4
-rw-r--r--core/lib/Drupal/Core/Plugin/Context/EntityContext.php62
-rw-r--r--core/lib/Drupal/Core/Plugin/Context/EntityContextDefinition.php116
-rw-r--r--core/lib/Drupal/Core/Plugin/ContextAwarePluginAssignmentTrait.php5
-rw-r--r--core/lib/Drupal/Core/Plugin/ContextAwarePluginBase.php2
-rw-r--r--core/lib/Drupal/Core/Plugin/DefaultPluginManager.php5
-rw-r--r--core/lib/Drupal/Core/Plugin/Discovery/AnnotatedClassDiscovery.php3
-rw-r--r--core/lib/Drupal/Core/Plugin/Discovery/ContainerDerivativeDiscoveryDecorator.php5
-rw-r--r--core/lib/Drupal/Core/Plugin/FilteredPluginManagerInterface.php36
-rw-r--r--core/lib/Drupal/Core/Plugin/FilteredPluginManagerTrait.php75
-rw-r--r--core/lib/Drupal/Core/Plugin/PluginBase.php2
-rw-r--r--core/lib/Drupal/Core/Plugin/PluginDependencyTrait.php36
-rw-r--r--core/lib/Drupal/Core/Plugin/plugin.api.php68
-rw-r--r--core/lib/Drupal/Core/ProxyBuilder/ProxyBuilder.php2
-rw-r--r--core/lib/Drupal/Core/ProxyClass/Field/FieldModuleUninstallValidator.php88
-rw-r--r--core/lib/Drupal/Core/Queue/DatabaseQueue.php2
-rw-r--r--core/lib/Drupal/Core/Queue/QueueFactory.php1
-rw-r--r--core/lib/Drupal/Core/Queue/QueueInterface.php2
-rw-r--r--core/lib/Drupal/Core/Render/Element.php4
-rw-r--r--core/lib/Drupal/Core/Render/Element/Details.php3
-rw-r--r--core/lib/Drupal/Core/Render/Element/Email.php2
-rw-r--r--core/lib/Drupal/Core/Render/Element/File.php2
-rw-r--r--core/lib/Drupal/Core/Render/Element/HtmlTag.php10
-rw-r--r--core/lib/Drupal/Core/Render/Element/Link.php6
-rw-r--r--core/lib/Drupal/Core/Render/Element/MachineName.php21
-rw-r--r--core/lib/Drupal/Core/Render/Element/Password.php2
-rw-r--r--core/lib/Drupal/Core/Render/Element/RenderElement.php2
-rw-r--r--core/lib/Drupal/Core/Render/Element/StatusMessages.php20
-rw-r--r--core/lib/Drupal/Core/Render/Element/SystemCompactLink.php2
-rw-r--r--core/lib/Drupal/Core/Render/Element/Tel.php2
-rw-r--r--core/lib/Drupal/Core/Render/Element/Textfield.php4
-rw-r--r--core/lib/Drupal/Core/Render/Element/Url.php2
-rw-r--r--core/lib/Drupal/Core/Render/Element/Weight.php5
-rw-r--r--core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php4
-rw-r--r--core/lib/Drupal/Core/Render/MainContent/DialogRenderer.php2
-rw-r--r--core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php2
-rw-r--r--core/lib/Drupal/Core/Render/MainContent/OffCanvasRenderer.php14
-rw-r--r--core/lib/Drupal/Core/Render/Markup.php1
-rw-r--r--core/lib/Drupal/Core/Render/Placeholder/ChainedPlaceholderStrategy.php4
-rw-r--r--core/lib/Drupal/Core/Render/PreviewFallbackInterface.php21
-rw-r--r--core/lib/Drupal/Core/Render/RenderCache.php2
-rw-r--r--core/lib/Drupal/Core/Render/Renderer.php32
-rw-r--r--core/lib/Drupal/Core/Render/theme.api.php14
-rw-r--r--core/lib/Drupal/Core/Routing/AccessAwareRouter.php2
-rw-r--r--core/lib/Drupal/Core/Routing/AccessAwareRouterInterface.php1
-rw-r--r--core/lib/Drupal/Core/Routing/BcRoute.php29
-rw-r--r--core/lib/Drupal/Core/Routing/CompiledRoute.php3
-rw-r--r--core/lib/Drupal/Core/Routing/ContentTypeHeaderMatcher.php7
-rw-r--r--core/lib/Drupal/Core/Routing/Enhancer/RouteEnhancerInterface.php2
-rw-r--r--core/lib/Drupal/Core/Routing/NullGenerator.php4
-rw-r--r--core/lib/Drupal/Core/Routing/RequestFormatRouteFilter.php63
-rw-r--r--core/lib/Drupal/Core/Routing/RouteFilterInterface.php2
-rw-r--r--core/lib/Drupal/Core/Routing/RouteMatch.php2
-rw-r--r--core/lib/Drupal/Core/Routing/RouteProvider.php54
-rw-r--r--core/lib/Drupal/Core/Routing/RouteProviderLazyBuilder.php40
-rw-r--r--core/lib/Drupal/Core/Routing/Router.php39
-rw-r--r--core/lib/Drupal/Core/Routing/StackedRouteMatchInterface.php2
-rw-r--r--core/lib/Drupal/Core/Routing/UrlGenerator.php7
-rw-r--r--core/lib/Drupal/Core/Security/RequestSanitizer.php168
-rw-r--r--core/lib/Drupal/Core/Session/AccountInterface.php15
-rw-r--r--core/lib/Drupal/Core/Session/AccountProxyInterface.php5
-rw-r--r--core/lib/Drupal/Core/Session/SessionConfiguration.php2
-rw-r--r--core/lib/Drupal/Core/Session/SessionManager.php15
-rw-r--r--core/lib/Drupal/Core/Session/UserSession.php2
-rw-r--r--core/lib/Drupal/Core/Session/WriteSafeSessionHandlerInterface.php6
-rw-r--r--core/lib/Drupal/Core/Site/Settings.php9
-rw-r--r--core/lib/Drupal/Core/StackMiddleware/NegotiationMiddleware.php8
-rw-r--r--core/lib/Drupal/Core/StackMiddleware/ReverseProxyMiddleware.php10
-rw-r--r--core/lib/Drupal/Core/State/State.php70
-rw-r--r--core/lib/Drupal/Core/StreamWrapper/LocalReadOnlyStream.php1
-rw-r--r--core/lib/Drupal/Core/StreamWrapper/PrivateStream.php2
-rw-r--r--core/lib/Drupal/Core/StreamWrapper/StreamWrapperInterface.php4
-rw-r--r--core/lib/Drupal/Core/StringTranslation/PluralTranslatableMarkup.php2
-rw-r--r--core/lib/Drupal/Core/StringTranslation/TranslatableMarkup.php13
-rw-r--r--core/lib/Drupal/Core/StringTranslation/TranslationInterface.php4
-rw-r--r--core/lib/Drupal/Core/TempStore/PrivateTempStore.php255
-rw-r--r--core/lib/Drupal/Core/TempStore/PrivateTempStoreFactory.php88
-rw-r--r--core/lib/Drupal/Core/TempStore/SharedTempStore.php279
-rw-r--r--core/lib/Drupal/Core/TempStore/SharedTempStoreFactory.php87
-rw-r--r--core/lib/Drupal/Core/TempStore/TempStoreException.php11
-rw-r--r--core/lib/Drupal/Core/Template/Loader/ThemeRegistryLoader.php6
-rw-r--r--core/lib/Drupal/Core/Template/TwigEnvironment.php31
-rw-r--r--core/lib/Drupal/Core/Template/TwigNodeTrans.php7
-rw-r--r--core/lib/Drupal/Core/Template/TwigNodeVisitor.php7
-rw-r--r--core/lib/Drupal/Core/Template/TwigSandboxPolicy.php12
-rw-r--r--core/lib/Drupal/Core/Test/AssertMailTrait.php6
-rw-r--r--core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php55
-rw-r--r--core/lib/Drupal/Core/Test/RefreshVariablesTrait.php38
-rw-r--r--core/lib/Drupal/Core/Test/TestDatabase.php42
-rw-r--r--core/lib/Drupal/Core/Test/TestSetupTrait.php3
-rw-r--r--core/lib/Drupal/Core/Test/TestStatus.php2
-rw-r--r--core/lib/Drupal/Core/Theme/ActiveTheme.php26
-rw-r--r--core/lib/Drupal/Core/Theme/Registry.php11
-rw-r--r--core/lib/Drupal/Core/Theme/ThemeAccessCheck.php1
-rw-r--r--core/lib/Drupal/Core/Theme/ThemeInitialization.php9
-rw-r--r--core/lib/Drupal/Core/TypedData/DataDefinition.php9
-rw-r--r--core/lib/Drupal/Core/TypedData/ListDataDefinition.php22
-rw-r--r--core/lib/Drupal/Core/TypedData/Plugin/DataType/ItemList.php2
-rw-r--r--core/lib/Drupal/Core/TypedData/Plugin/DataType/Map.php2
-rw-r--r--core/lib/Drupal/Core/TypedData/TranslatableInterface.php17
-rw-r--r--core/lib/Drupal/Core/TypedData/TypedData.php6
-rw-r--r--core/lib/Drupal/Core/TypedData/TypedDataManager.php7
-rw-r--r--core/lib/Drupal/Core/TypedData/Validation/RecursiveContextualValidator.php8
-rw-r--r--core/lib/Drupal/Core/Update/UpdateKernel.php10
-rw-r--r--core/lib/Drupal/Core/Update/UpdateRegistry.php2
-rw-r--r--core/lib/Drupal/Core/Updater/Module.php7
-rw-r--r--core/lib/Drupal/Core/Updater/Theme.php2
-rw-r--r--core/lib/Drupal/Core/Updater/Updater.php3
-rw-r--r--core/lib/Drupal/Core/Utility/Error.php4
-rw-r--r--core/lib/Drupal/Core/Utility/LinkGeneratorInterface.php2
-rw-r--r--core/lib/Drupal/Core/Utility/UnroutedUrlAssembler.php21
-rw-r--r--core/lib/Drupal/Core/Validation/ConstraintManager.php1
-rw-r--r--core/lib/Drupal/Core/Validation/DrupalTranslator.php15
-rw-r--r--core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/PrimitiveTypeConstraintValidator.php2
-rw-r--r--core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldValueValidator.php3
455 files changed, 11750 insertions, 2340 deletions
diff --git a/core/lib/Drupal/Core/Access/AccessManagerInterface.php b/core/lib/Drupal/Core/Access/AccessManagerInterface.php
index bbb1ec9..2ea6a6a 100644
--- a/core/lib/Drupal/Core/Access/AccessManagerInterface.php
+++ b/core/lib/Drupal/Core/Access/AccessManagerInterface.php
@@ -38,7 +38,7 @@ interface AccessManagerInterface {
/**
* Execute access checks against the incoming request.
*
- * @param Request $request
+ * @param \Symfony\Component\HttpFoundation\Request $request
* The incoming request.
* @param \Drupal\Core\Session\AccountInterface $account
* (optional) Run access checks for this account. Defaults to the current
diff --git a/core/lib/Drupal/Core/Access/AccessResult.php b/core/lib/Drupal/Core/Access/AccessResult.php
index f4962ec..3abbf33 100644
--- a/core/lib/Drupal/Core/Access/AccessResult.php
+++ b/core/lib/Drupal/Core/Access/AccessResult.php
@@ -39,7 +39,7 @@ abstract class AccessResult implements AccessResultInterface, RefinableCacheable
* isNeutral() will be TRUE.
*/
public static function neutral($reason = NULL) {
- assert('is_string($reason) || is_null($reason)');
+ assert(is_string($reason) || is_null($reason));
return new AccessResultNeutral($reason);
}
@@ -64,7 +64,7 @@ abstract class AccessResult implements AccessResultInterface, RefinableCacheable
* isForbidden() will be TRUE.
*/
public static function forbidden($reason = NULL) {
- assert('is_string($reason) || is_null($reason)');
+ assert(is_string($reason) || is_null($reason));
return new AccessResultForbidden($reason);
}
@@ -336,10 +336,10 @@ abstract class AccessResult implements AccessResultInterface, RefinableCacheable
$merge_other = TRUE;
}
- if ($this->isForbidden() && $this instanceof AccessResultReasonInterface) {
+ if ($this->isForbidden() && $this instanceof AccessResultReasonInterface && !is_null($this->getReason())) {
$result->setReason($this->getReason());
}
- elseif ($other->isForbidden() && $other instanceof AccessResultReasonInterface) {
+ elseif ($other->isForbidden() && $other instanceof AccessResultReasonInterface && !is_null($other->getReason())) {
$result->setReason($other->getReason());
}
}
@@ -353,14 +353,13 @@ abstract class AccessResult implements AccessResultInterface, RefinableCacheable
$result = static::neutral();
if (!$this->isNeutral() || ($this->getCacheMaxAge() === 0 && $other->isNeutral()) || ($this->getCacheMaxAge() !== 0 && $other instanceof CacheableDependencyInterface && $other->getCacheMaxAge() !== 0)) {
$merge_other = TRUE;
- if ($other instanceof AccessResultReasonInterface) {
- $result->setReason($other->getReason());
- }
}
- else {
- if ($this instanceof AccessResultReasonInterface) {
- $result->setReason($this->getReason());
- }
+
+ if ($this instanceof AccessResultReasonInterface && !is_null($this->getReason())) {
+ $result->setReason($this->getReason());
+ }
+ elseif ($other instanceof AccessResultReasonInterface && !is_null($other->getReason())) {
+ $result->setReason($other->getReason());
}
}
$result->inheritCacheability($this);
@@ -427,9 +426,9 @@ abstract class AccessResult implements AccessResultInterface, RefinableCacheable
/**
* Inherits the cacheability of the other access result, if any.
*
- * inheritCacheability() differs from addCacheableDependency() in how it
- * handles max-age, because it is designed to inherit the cacheability of the
- * second operand in the andIf() and orIf() operations. There, the situation
+ * This method differs from addCacheableDependency() in how it handles
+ * max-age, because it is designed to inherit the cacheability of the second
+ * operand in the andIf() and orIf() operations. There, the situation
* "allowed, max-age=0 OR allowed, max-age=1000" needs to yield max-age 1000
* as the end result.
*
diff --git a/core/lib/Drupal/Core/Access/AccessResultForbidden.php b/core/lib/Drupal/Core/Access/AccessResultForbidden.php
index 3edae61..dbdbafe 100644
--- a/core/lib/Drupal/Core/Access/AccessResultForbidden.php
+++ b/core/lib/Drupal/Core/Access/AccessResultForbidden.php
@@ -24,7 +24,6 @@ class AccessResultForbidden extends AccessResult implements AccessResultReasonIn
$this->reason = $reason;
}
-
/**
* {@inheritdoc}
*/
diff --git a/core/lib/Drupal/Core/Access/CheckProvider.php b/core/lib/Drupal/Core/Access/CheckProvider.php
index d6ba1cf..51423bd 100644
--- a/core/lib/Drupal/Core/Access/CheckProvider.php
+++ b/core/lib/Drupal/Core/Access/CheckProvider.php
@@ -142,6 +142,7 @@ class CheckProvider implements CheckProviderInterface, ContainerAwareInterface {
return $checks;
}
+
/**
* Compiles a mapping of requirement keys to access checker service IDs.
*/
diff --git a/core/lib/Drupal/Core/Access/CheckProviderInterface.php b/core/lib/Drupal/Core/Access/CheckProviderInterface.php
index a94a92c..35e3c18 100644
--- a/core/lib/Drupal/Core/Access/CheckProviderInterface.php
+++ b/core/lib/Drupal/Core/Access/CheckProviderInterface.php
@@ -15,7 +15,6 @@ use Symfony\Component\Routing\RouteCollection;
*/
interface CheckProviderInterface {
-
/**
* For each route, saves a list of applicable access checks to the route.
*
diff --git a/core/lib/Drupal/Core/Access/CustomAccessCheck.php b/core/lib/Drupal/Core/Access/CustomAccessCheck.php
index e7a3b08..353d09c 100644
--- a/core/lib/Drupal/Core/Access/CustomAccessCheck.php
+++ b/core/lib/Drupal/Core/Access/CustomAccessCheck.php
@@ -61,7 +61,14 @@ class CustomAccessCheck implements RoutingAccessInterface {
* The access result.
*/
public function access(Route $route, RouteMatchInterface $route_match, AccountInterface $account) {
- $callable = $this->controllerResolver->getControllerFromDefinition($route->getRequirement('_custom_access'));
+ try {
+ $callable = $this->controllerResolver->getControllerFromDefinition($route->getRequirement('_custom_access'));
+ }
+ catch (\InvalidArgumentException $e) {
+ // The custom access controller method was not found.
+ throw new \BadMethodCallException(sprintf('The "%s" method is not callable as a _custom_access callback in route "%s"', $route->getRequirement('_custom_access'), $route->getPath()));
+ }
+
$arguments_resolver = $this->argumentsResolverFactory->getArgumentsResolver($route_match, $account);
$arguments = $arguments_resolver->getArguments($callable);
diff --git a/core/lib/Drupal/Core/Action/Plugin/Action/DeleteAction.php b/core/lib/Drupal/Core/Action/Plugin/Action/DeleteAction.php
new file mode 100644
index 0000000..fcf76a4
--- /dev/null
+++ b/core/lib/Drupal/Core/Action/Plugin/Action/DeleteAction.php
@@ -0,0 +1,99 @@
+<?php
+
+namespace Drupal\Core\Action\Plugin\Action;
+
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\TempStore\PrivateTempStoreFactory;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Redirects to an entity deletion form.
+ *
+ * @Action(
+ * id = "entity:delete_action",
+ * action_label = @Translation("Delete"),
+ * deriver = "Drupal\Core\Action\Plugin\Action\Derivative\EntityDeleteActionDeriver",
+ * )
+ */
+class DeleteAction extends EntityActionBase {
+
+ /**
+ * The tempstore object.
+ *
+ * @var \Drupal\Core\TempStore\SharedTempStore
+ */
+ protected $tempStore;
+
+ /**
+ * The current user.
+ *
+ * @var \Drupal\Core\Session\AccountInterface
+ */
+ protected $currentUser;
+
+ /**
+ * Constructs a new DeleteAction object.
+ *
+ * @param array $configuration
+ * A configuration array containing information about the plugin instance.
+ * @param string $plugin_id
+ * The plugin ID for the plugin instance.
+ * @param mixed $plugin_definition
+ * The plugin implementation definition.
+ * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+ * The entity type manager.
+ * @param \Drupal\Core\TempStore\PrivateTempStoreFactory $temp_store_factory
+ * The tempstore factory.
+ * @param \Drupal\Core\Session\AccountInterface $current_user
+ * Current user.
+ */
+ public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, PrivateTempStoreFactory $temp_store_factory, AccountInterface $current_user) {
+ $this->currentUser = $current_user;
+ $this->tempStore = $temp_store_factory->get('entity_delete_multiple_confirm');
+
+ parent::__construct($configuration, $plugin_id, $plugin_definition, $entity_type_manager);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+ return new static(
+ $configuration,
+ $plugin_id,
+ $plugin_definition,
+ $container->get('entity_type.manager'),
+ $container->get('tempstore.private'),
+ $container->get('current_user')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function executeMultiple(array $entities) {
+ /** @var \Drupal\Core\Entity\EntityInterface[] $entities */
+ $selection = [];
+ foreach ($entities as $entity) {
+ $langcode = $entity->language()->getId();
+ $selection[$entity->id()][$langcode] = $langcode;
+ }
+ $this->tempStore->set($this->currentUser->id() . ':' . $this->getPluginDefinition()['type'], $selection);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function execute($object = NULL) {
+ $this->executeMultiple([$object]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
+ return $object->access('delete', $account, $return_as_object);
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Action/Plugin/Action/Derivative/EntityActionDeriverBase.php b/core/lib/Drupal/Core/Action/Plugin/Action/Derivative/EntityActionDeriverBase.php
new file mode 100644
index 0000000..5cd529a
--- /dev/null
+++ b/core/lib/Drupal/Core/Action/Plugin/Action/Derivative/EntityActionDeriverBase.php
@@ -0,0 +1,99 @@
+<?php
+
+namespace Drupal\Core\Action\Plugin\Action\Derivative;
+
+use Drupal\Component\Plugin\Derivative\DeriverBase;
+use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\StringTranslation\TranslationInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides a base action for each entity type with specific interfaces.
+ */
+abstract class EntityActionDeriverBase extends DeriverBase implements ContainerDeriverInterface {
+
+ use StringTranslationTrait;
+
+ /**
+ * The entity type manager.
+ *
+ * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+ */
+ protected $entityTypeManager;
+
+ /**
+ * Constructs a new EntityActionDeriverBase object.
+ *
+ * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+ * The entity type manager.
+ * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
+ * The string translation service.
+ */
+ public function __construct(EntityTypeManagerInterface $entity_type_manager, TranslationInterface $string_translation) {
+ $this->entityTypeManager = $entity_type_manager;
+ $this->stringTranslation = $string_translation;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container, $base_plugin_id) {
+ return new static(
+ $container->get('entity_type.manager'),
+ $container->get('string_translation')
+ );
+ }
+
+ /**
+ * Indicates whether the deriver can be used for the provided entity type.
+ *
+ * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+ * The entity type.
+ *
+ * @return bool
+ * TRUE if the entity type can be used, FALSE otherwise.
+ */
+ abstract protected function isApplicable(EntityTypeInterface $entity_type);
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDerivativeDefinitions($base_plugin_definition) {
+ if (empty($this->derivatives)) {
+ $definitions = [];
+ foreach ($this->getApplicableEntityTypes() as $entity_type_id => $entity_type) {
+ $definition = $base_plugin_definition;
+ $definition['type'] = $entity_type_id;
+ $definition['label'] = sprintf('%s %s', $base_plugin_definition['action_label'], $entity_type->getSingularLabel());
+ $definitions[$entity_type_id] = $definition;
+ }
+ $this->derivatives = $definitions;
+ }
+
+ return parent::getDerivativeDefinitions($base_plugin_definition);
+ }
+
+ /**
+ * Gets a list of applicable entity types.
+ *
+ * The list consists of all entity types which match the conditions for the
+ * given deriver.
+ * For example, if the action applies to entities that are publishable,
+ * this method will find all entity types that are publishable.
+ *
+ * @return \Drupal\Core\Entity\EntityTypeInterface[]
+ * The applicable entity types, keyed by entity type ID.
+ */
+ protected function getApplicableEntityTypes() {
+ $entity_types = $this->entityTypeManager->getDefinitions();
+ $entity_types = array_filter($entity_types, function (EntityTypeInterface $entity_type) {
+ return $this->isApplicable($entity_type);
+ });
+
+ return $entity_types;
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Action/Plugin/Action/Derivative/EntityChangedActionDeriver.php b/core/lib/Drupal/Core/Action/Plugin/Action/Derivative/EntityChangedActionDeriver.php
new file mode 100644
index 0000000..2df090b
--- /dev/null
+++ b/core/lib/Drupal/Core/Action/Plugin/Action/Derivative/EntityChangedActionDeriver.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace Drupal\Core\Action\Plugin\Action\Derivative;
+
+use Drupal\Core\Entity\EntityChangedInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+
+/**
+ * Provides an action deriver that finds entity types of EntityChangedInterface.
+ *
+ * @see \Drupal\Core\Action\Plugin\Action\SaveAction
+ */
+class EntityChangedActionDeriver extends EntityActionDeriverBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function isApplicable(EntityTypeInterface $entity_type) {
+ return $entity_type->entityClassImplements(EntityChangedInterface::class);
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Action/Plugin/Action/Derivative/EntityDeleteActionDeriver.php b/core/lib/Drupal/Core/Action/Plugin/Action/Derivative/EntityDeleteActionDeriver.php
new file mode 100644
index 0000000..4be4588
--- /dev/null
+++ b/core/lib/Drupal/Core/Action/Plugin/Action/Derivative/EntityDeleteActionDeriver.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace Drupal\Core\Action\Plugin\Action\Derivative;
+
+use Drupal\Core\Entity\EntityTypeInterface;
+
+/**
+ * Provides an action deriver that finds entity types with delete form.
+ *
+ * @see \Drupal\Core\Action\Plugin\Action\DeleteAction
+ */
+class EntityDeleteActionDeriver extends EntityActionDeriverBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDerivativeDefinitions($base_plugin_definition) {
+ if (empty($this->derivatives)) {
+ $definitions = [];
+ foreach ($this->getApplicableEntityTypes() as $entity_type_id => $entity_type) {
+ $definition = $base_plugin_definition;
+ $definition['type'] = $entity_type_id;
+ $definition['label'] = $this->t('Delete @entity_type', ['@entity_type' => $entity_type->getSingularLabel()]);
+ $definition['confirm_form_route_name'] = 'entity.' . $entity_type->id() . '.delete_multiple_form';
+ $definitions[$entity_type_id] = $definition;
+ }
+ $this->derivatives = $definitions;
+ }
+
+ return $this->derivatives;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function isApplicable(EntityTypeInterface $entity_type) {
+ return $entity_type->hasLinkTemplate('delete-multiple-form');
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Action/Plugin/Action/Derivative/EntityPublishedActionDeriver.php b/core/lib/Drupal/Core/Action/Plugin/Action/Derivative/EntityPublishedActionDeriver.php
new file mode 100644
index 0000000..05548ee
--- /dev/null
+++ b/core/lib/Drupal/Core/Action/Plugin/Action/Derivative/EntityPublishedActionDeriver.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace Drupal\Core\Action\Plugin\Action\Derivative;
+
+use Drupal\Core\Entity\EntityPublishedInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+
+/**
+ * Provides an action deriver that finds publishable entity types.
+ *
+ * @see \Drupal\Core\Action\Plugin\Action\PublishAction
+ * @see \Drupal\Core\Action\Plugin\Action\UnpublishAction
+ */
+class EntityPublishedActionDeriver extends EntityActionDeriverBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function isApplicable(EntityTypeInterface $entity_type) {
+ return $entity_type->entityClassImplements(EntityPublishedInterface::class);
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Action/Plugin/Action/EntityActionBase.php b/core/lib/Drupal/Core/Action/Plugin/Action/EntityActionBase.php
new file mode 100644
index 0000000..2624024
--- /dev/null
+++ b/core/lib/Drupal/Core/Action/Plugin/Action/EntityActionBase.php
@@ -0,0 +1,62 @@
+<?php
+
+namespace Drupal\Core\Action\Plugin\Action;
+
+use Drupal\Component\Plugin\DependentPluginInterface;
+use Drupal\Core\Action\ActionBase;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Base class for entity-based actions.
+ */
+abstract class EntityActionBase extends ActionBase implements DependentPluginInterface, ContainerFactoryPluginInterface {
+
+ /**
+ * The entity type manager.
+ *
+ * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+ */
+ protected $entityTypeManager;
+
+ /**
+ * Constructs a EntityActionBase object.
+ *
+ * @param mixed[] $configuration
+ * A configuration array containing information about the plugin instance.
+ * @param string $plugin_id
+ * The plugin ID for the plugin instance.
+ * @param mixed $plugin_definition
+ * The plugin implementation definition.
+ * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+ * The entity type manager.
+ */
+ public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager) {
+ parent::__construct($configuration, $plugin_id, $plugin_definition);
+ $this->entityTypeManager = $entity_type_manager;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+ return new static(
+ $configuration,
+ $plugin_id,
+ $plugin_definition,
+ $container->get('entity_type.manager')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function calculateDependencies() {
+ $module_name = $this->entityTypeManager
+ ->getDefinition($this->getPluginDefinition()['type'])
+ ->getProvider();
+ return ['module' => [$module_name]];
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Action/Plugin/Action/PublishAction.php b/core/lib/Drupal/Core/Action/Plugin/Action/PublishAction.php
new file mode 100644
index 0000000..8d0cf75
--- /dev/null
+++ b/core/lib/Drupal/Core/Action/Plugin/Action/PublishAction.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Drupal\Core\Action\Plugin\Action;
+
+use Drupal\Core\Session\AccountInterface;
+
+/**
+ * Publishes an entity.
+ *
+ * @Action(
+ * id = "entity:publish_action",
+ * action_label = @Translation("Publish"),
+ * deriver = "Drupal\Core\Action\Plugin\Action\Derivative\EntityPublishedActionDeriver",
+ * )
+ */
+class PublishAction extends EntityActionBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function execute($entity = NULL) {
+ $entity->setPublished()->save();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
+ $key = $object->getEntityType()->getKey('published');
+
+ /** @var \Drupal\Core\Entity\EntityInterface $object */
+ $result = $object->access('update', $account, TRUE)
+ ->andIf($object->$key->access('edit', $account, TRUE));
+
+ return $return_as_object ? $result : $result->isAllowed();
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Action/Plugin/Action/SaveAction.php b/core/lib/Drupal/Core/Action/Plugin/Action/SaveAction.php
new file mode 100644
index 0000000..fc63e5c
--- /dev/null
+++ b/core/lib/Drupal/Core/Action/Plugin/Action/SaveAction.php
@@ -0,0 +1,79 @@
+<?php
+
+namespace Drupal\Core\Action\Plugin\Action;
+
+use Drupal\Component\Datetime\TimeInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Session\AccountInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides an action that can save any entity.
+ *
+ * @Action(
+ * id = "entity:save_action",
+ * action_label = @Translation("Save"),
+ * deriver = "Drupal\Core\Action\Plugin\Action\Derivative\EntityChangedActionDeriver",
+ * )
+ */
+class SaveAction extends EntityActionBase {
+
+ /**
+ * The time service.
+ *
+ * @var \Drupal\Component\Datetime\TimeInterface
+ */
+ protected $time;
+
+ /**
+ * Constructs a SaveAction object.
+ *
+ * @param mixed[] $configuration
+ * A configuration array containing information about the plugin instance.
+ * @param string $plugin_id
+ * The plugin ID for the plugin instance.
+ * @param mixed $plugin_definition
+ * The plugin implementation definition.
+ * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+ * The entity type manager.
+ * @param \Drupal\Component\Datetime\TimeInterface $time
+ * The time service.
+ */
+ public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, TimeInterface $time) {
+ parent::__construct($configuration, $plugin_id, $plugin_definition, $entity_type_manager);
+ $this->time = $time;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+ return new static(
+ $configuration,
+ $plugin_id,
+ $plugin_definition,
+ $container->get('entity_type.manager'),
+ $container->get('datetime.time')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function execute($entity = NULL) {
+ $entity->setChangedTime($this->time->getRequestTime())->save();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
+ // It's not necessary to check the changed field access here, because
+ // Drupal\Core\Field\ChangedFieldItemList would anyway return 'not allowed'.
+ // Also changing the changed field value is only a workaround to trigger an
+ // entity resave. Without a field change, this would not be possible.
+ /** @var \Drupal\Core\Entity\EntityInterface $object */
+ return $object->access('update', $account, $return_as_object);
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Action/Plugin/Action/UnpublishAction.php b/core/lib/Drupal/Core/Action/Plugin/Action/UnpublishAction.php
new file mode 100644
index 0000000..bb3f6c2
--- /dev/null
+++ b/core/lib/Drupal/Core/Action/Plugin/Action/UnpublishAction.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Drupal\Core\Action\Plugin\Action;
+
+use Drupal\Core\Session\AccountInterface;
+
+/**
+ * Unpublishes an entity.
+ *
+ * @Action(
+ * id = "entity:unpublish_action",
+ * action_label = @Translation("Unpublish"),
+ * deriver = "Drupal\Core\Action\Plugin\Action\Derivative\EntityPublishedActionDeriver",
+ * )
+ */
+class UnpublishAction extends EntityActionBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function execute($entity = NULL) {
+ $entity->setUnpublished()->save();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
+ $key = $object->getEntityType()->getKey('published');
+
+ /** @var \Drupal\Core\Entity\EntityInterface $object */
+ $result = $object->access('update', $account, TRUE)
+ ->andIf($object->$key->access('edit', $account, TRUE));
+
+ return $return_as_object ? $result : $result->isAllowed();
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Ajax/AjaxFormHelperTrait.php b/core/lib/Drupal/Core/Ajax/AjaxFormHelperTrait.php
new file mode 100644
index 0000000..49932df
--- /dev/null
+++ b/core/lib/Drupal/Core/Ajax/AjaxFormHelperTrait.php
@@ -0,0 +1,56 @@
+<?php
+
+namespace Drupal\Core\Ajax;
+
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Provides a helper to for submitting an AJAX form.
+ *
+ * @internal
+ */
+trait AjaxFormHelperTrait {
+
+ use AjaxHelperTrait;
+
+ /**
+ * Submit form dialog #ajax callback.
+ *
+ * @param array $form
+ * An associative array containing the structure of the form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the form.
+ *
+ * @return \Drupal\Core\Ajax\AjaxResponse
+ * An AJAX response that display validation error messages or represents a
+ * successful submission.
+ */
+ public function ajaxSubmit(array &$form, FormStateInterface $form_state) {
+ if ($form_state->hasAnyErrors()) {
+ $form['status_messages'] = [
+ '#type' => 'status_messages',
+ '#weight' => -1000,
+ ];
+ $response = new AjaxResponse();
+ $response->addCommand(new ReplaceCommand('[data-drupal-selector="' . $form['#attributes']['data-drupal-selector'] . '"]', $form));
+ }
+ else {
+ $response = $this->successfulAjaxSubmit($form, $form_state);
+ }
+ return $response;
+ }
+
+ /**
+ * Allows the form to respond to a successful AJAX submission.
+ *
+ * @param array $form
+ * An associative array containing the structure of the form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the form.
+ *
+ * @return \Drupal\Core\Ajax\AjaxResponse
+ * An AJAX response.
+ */
+ abstract protected function successfulAjaxSubmit(array $form, FormStateInterface $form_state);
+
+}
diff --git a/core/lib/Drupal/Core/Ajax/AjaxHelperTrait.php b/core/lib/Drupal/Core/Ajax/AjaxHelperTrait.php
new file mode 100644
index 0000000..287abae
--- /dev/null
+++ b/core/lib/Drupal/Core/Ajax/AjaxHelperTrait.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace Drupal\Core\Ajax;
+
+use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
+
+/**
+ * Provides a helper to determine if the current request is via AJAX.
+ *
+ * @internal
+ */
+trait AjaxHelperTrait {
+
+ /**
+ * Determines if the current request is via AJAX.
+ *
+ * @return bool
+ * TRUE if the current request is via AJAX, FALSE otherwise.
+ */
+ protected function isAjax() {
+ foreach (['drupal_ajax', 'drupal_modal', 'drupal_dialog'] as $wrapper) {
+ if (strpos($this->getRequestWrapperFormat(), $wrapper) !== FALSE) {
+ return TRUE;
+ }
+ }
+ return FALSE;
+ }
+
+ /**
+ * Gets the wrapper format of the current request.
+ *
+ * @string
+ * The wrapper format.
+ */
+ protected function getRequestWrapperFormat() {
+ return \Drupal::request()->get(MainContentViewSubscriber::WRAPPER_FORMAT);
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Ajax/OpenDialogCommand.php b/core/lib/Drupal/Core/Ajax/OpenDialogCommand.php
index 2b0fcdd..623d396 100644
--- a/core/lib/Drupal/Core/Ajax/OpenDialogCommand.php
+++ b/core/lib/Drupal/Core/Ajax/OpenDialogCommand.php
@@ -119,7 +119,7 @@ class OpenDialogCommand implements CommandInterface, CommandWithAttachedAssetsIn
* The new title of the dialog.
*/
public function setDialogTitle($title) {
- $this->setDialogOptions('title', $title);
+ $this->setDialogOption('title', $title);
}
/**
diff --git a/core/lib/Drupal/Core/Ajax/OpenModalDialogCommand.php b/core/lib/Drupal/Core/Ajax/OpenModalDialogCommand.php
index 53d6e82..a55b0ea 100644
--- a/core/lib/Drupal/Core/Ajax/OpenModalDialogCommand.php
+++ b/core/lib/Drupal/Core/Ajax/OpenModalDialogCommand.php
@@ -8,6 +8,7 @@ namespace Drupal\Core\Ajax;
* @ingroup ajax
*/
class OpenModalDialogCommand extends OpenDialogCommand {
+
/**
* Constructs an OpenModalDialog object.
*
diff --git a/core/lib/Drupal/Core/Ajax/OpenOffCanvasDialogCommand.php b/core/lib/Drupal/Core/Ajax/OpenOffCanvasDialogCommand.php
index da6a26e..78c406b 100644
--- a/core/lib/Drupal/Core/Ajax/OpenOffCanvasDialogCommand.php
+++ b/core/lib/Drupal/Core/Ajax/OpenOffCanvasDialogCommand.php
@@ -34,19 +34,22 @@ class OpenOffCanvasDialogCommand extends OpenDialogCommand {
* (optional) Custom settings that will be passed to the Drupal behaviors
* on the content of the dialog. If left empty, the settings will be
* populated automatically from the current request.
+ * @param string $position
+ * (optional) The position to render the off-canvas dialog.
*/
- public function __construct($title, $content, array $dialog_options = [], $settings = NULL) {
+ public function __construct($title, $content, array $dialog_options = [], $settings = NULL, $position = 'side') {
parent::__construct('#drupal-off-canvas', $title, $content, $dialog_options, $settings);
$this->dialogOptions['modal'] = FALSE;
$this->dialogOptions['autoResize'] = FALSE;
$this->dialogOptions['resizable'] = 'w';
$this->dialogOptions['draggable'] = FALSE;
$this->dialogOptions['drupalAutoButtons'] = FALSE;
+ $this->dialogOptions['drupalOffCanvasPosition'] = $position;
// @todo drupal.ajax.js does not respect drupalAutoButtons properly, pass an
// empty set of buttons until https://www.drupal.org/node/2793343 is in.
$this->dialogOptions['buttons'] = [];
if (empty($dialog_options['dialogClass'])) {
- $this->dialogOptions['dialogClass'] = 'ui-dialog-off-canvas';
+ $this->dialogOptions['dialogClass'] = "ui-dialog-off-canvas ui-dialog-position-$position";
}
// If no width option is provided then use the default width to avoid the
// dialog staying at the width of the previous instance when opened
diff --git a/core/lib/Drupal/Core/Annotation/ContextDefinition.php b/core/lib/Drupal/Core/Annotation/ContextDefinition.php
index 0439752..9e28d5a 100644
--- a/core/lib/Drupal/Core/Annotation/ContextDefinition.php
+++ b/core/lib/Drupal/Core/Annotation/ContextDefinition.php
@@ -116,11 +116,37 @@ class ContextDefinition extends Plugin {
if (isset($values['class']) && !in_array('Drupal\Core\Plugin\Context\ContextDefinitionInterface', class_implements($values['class']))) {
throw new \Exception('ContextDefinition class must implement \Drupal\Core\Plugin\Context\ContextDefinitionInterface.');
}
- $class = isset($values['class']) ? $values['class'] : 'Drupal\Core\Plugin\Context\ContextDefinition';
+
+ $class = $this->getDefinitionClass($values);
$this->definition = new $class($values['value'], $values['label'], $values['required'], $values['multiple'], $values['description'], $values['default_value']);
}
/**
+ * Determines the context definition class to use.
+ *
+ * If the annotation specifies a specific context definition class, we use
+ * that. Otherwise, we use \Drupal\Core\Plugin\Context\EntityContextDefinition
+ * if the data type starts with 'entity:', since it contains specialized logic
+ * specific to entities. Otherwise, we fall back to the generic
+ * \Drupal\Core\Plugin\Context\ContextDefinition class.
+ *
+ * @param array $values
+ * The annotation values.
+ *
+ * @return string
+ * The fully-qualified name of the context definition class.
+ */
+ protected function getDefinitionClass(array $values) {
+ if (isset($values['class'])) {
+ return $values['class'];
+ }
+ if (strpos($values['value'], 'entity:') === 0) {
+ return 'Drupal\Core\Plugin\Context\EntityContextDefinition';
+ }
+ return 'Drupal\Core\Plugin\Context\ContextDefinition';
+ }
+
+ /**
* Returns the value of an annotation.
*
* @return \Drupal\Core\Plugin\Context\ContextDefinitionInterface
diff --git a/core/lib/Drupal/Core/Archiver/ArchiveTar.php b/core/lib/Drupal/Core/Archiver/ArchiveTar.php
index 24085ce..716511e 100644
--- a/core/lib/Drupal/Core/Archiver/ArchiveTar.php
+++ b/core/lib/Drupal/Core/Archiver/ArchiveTar.php
@@ -49,7 +49,7 @@
* The following changes have been done:
* Added namespace Drupal\Core\Archiver.
* Removed require_once 'PEAR.php'.
- * Added defintion of OS_WINDOWS taken from PEAR.php.
+ * Added definition of OS_WINDOWS taken from PEAR.php.
* Renamed class to ArchiveTar.
* Removed extends PEAR from class.
* Removed call parent:: __construct().
@@ -181,7 +181,7 @@ class ArchiveTar
if ($data == "\37\213") {
$this->_compress = true;
$this->_compress_type = 'gz';
- // No sure it's enought for a magic code ....
+ // Not sure it's enough for a magic code ....
} elseif ($data == "BZ") {
$this->_compress = true;
$this->_compress_type = 'bz2';
@@ -577,7 +577,7 @@ class ArchiveTar
* indicated by $p_path. When relevant the memorized path of the
* files/dir can be modified by removing the $p_remove_path path at the
* beginning of the file/dir path.
- * While extracting a file, if the directory path does not exists it is
+ * While extracting a file, if the directory path does not exist it is
* created.
* While extracting a file, if the file already exists it is replaced
* without looking for last modification date.
@@ -2385,7 +2385,7 @@ class ArchiveTar
/**
* Compress path by changing for example "/dir/foo/../bar" to "/dir/bar",
- * rand emove double slashes.
+ * and remove double slashes.
*
* @param string $p_dir path to reduce
*
diff --git a/core/lib/Drupal/Core/Asset/CssOptimizer.php b/core/lib/Drupal/Core/Asset/CssOptimizer.php
index 4ad7ba1..94c2240 100644
--- a/core/lib/Drupal/Core/Asset/CssOptimizer.php
+++ b/core/lib/Drupal/Core/Asset/CssOptimizer.php
@@ -119,7 +119,7 @@ class CssOptimizer implements AssetOptimizerInterface {
// If a BOM is found, convert the file to UTF-8, then use substr() to
// remove the BOM from the result.
if ($encoding = (Unicode::encodingFromBOM($contents))) {
- $contents = Unicode::substr(Unicode::convertToUtf8($contents, $encoding), 1);
+ $contents = mb_substr(Unicode::convertToUtf8($contents, $encoding), 1);
}
// If no BOM, check for fallback encoding. Per CSS spec the regex is very strict.
elseif (preg_match('/^@charset "([^"]+)";/', $contents, $matches)) {
@@ -189,7 +189,7 @@ class CssOptimizer implements AssetOptimizerInterface {
if ($optimize) {
// Perform some safe CSS optimizations.
// Regexp to match comment blocks.
- $comment = '/\*[^*]*\*+(?:[^/*][^*]*\*+)*/';
+ $comment = '/\*[^*]*\*+(?:[^/*][^*]*\*+)*/';
// Regexp to match double quoted strings.
$double_quot = '"[^"\\\\]*(?:\\\\.[^"\\\\]*)*"';
// Regexp to match single quoted strings.
diff --git a/core/lib/Drupal/Core/Asset/JsOptimizer.php b/core/lib/Drupal/Core/Asset/JsOptimizer.php
index 6b8c217..243b402 100644
--- a/core/lib/Drupal/Core/Asset/JsOptimizer.php
+++ b/core/lib/Drupal/Core/Asset/JsOptimizer.php
@@ -24,7 +24,7 @@ class JsOptimizer implements AssetOptimizerInterface {
// remove the BOM from the result.
$data = file_get_contents($js_asset['data']);
if ($encoding = (Unicode::encodingFromBOM($data))) {
- $data = Unicode::substr(Unicode::convertToUtf8($data, $encoding), 1);
+ $data = mb_substr(Unicode::convertToUtf8($data, $encoding), 1);
}
// If no BOM is found, check for the charset attribute.
elseif (isset($js_asset['attributes']['charset'])) {
diff --git a/core/lib/Drupal/Core/Asset/LibraryDependencyResolver.php b/core/lib/Drupal/Core/Asset/LibraryDependencyResolver.php
index 95fca18..b84b8fa 100644
--- a/core/lib/Drupal/Core/Asset/LibraryDependencyResolver.php
+++ b/core/lib/Drupal/Core/Asset/LibraryDependencyResolver.php
@@ -65,6 +65,8 @@ class LibraryDependencyResolver implements LibraryDependencyResolverInterface {
* {@inheritdoc}
*/
public function getMinimalRepresentativeSubset(array $libraries) {
+ assert(count($libraries) === count(array_unique($libraries)), '$libraries can\'t contain duplicate items.');
+
$minimal = [];
// Determine each library's dependencies.
diff --git a/core/lib/Drupal/Core/Asset/LibraryDiscovery.php b/core/lib/Drupal/Core/Asset/LibraryDiscovery.php
index a49ea97..39c3e2b 100644
--- a/core/lib/Drupal/Core/Asset/LibraryDiscovery.php
+++ b/core/lib/Drupal/Core/Asset/LibraryDiscovery.php
@@ -19,7 +19,7 @@ class LibraryDiscovery implements LibraryDiscoveryInterface {
/**
* The final library definitions, statically cached.
*
- * hook_library_info_alter() and hook_js_settings_alter() allows modules
+ * Hooks hook_library_info_alter() and hook_js_settings_alter() allow modules
* and themes to dynamically alter a library definition (once per request).
*
* @var array
diff --git a/core/lib/Drupal/Core/Asset/LibraryDiscoveryParser.php b/core/lib/Drupal/Core/Asset/LibraryDiscoveryParser.php
index 91f702f..07df709 100644
--- a/core/lib/Drupal/Core/Asset/LibraryDiscoveryParser.php
+++ b/core/lib/Drupal/Core/Asset/LibraryDiscoveryParser.php
@@ -129,11 +129,11 @@ class LibraryDiscoveryParser {
// properly resolve dependencies for all (css) libraries per category,
// and only once prior to rendering out an HTML page.
if ($type == 'css' && !empty($library[$type])) {
- assert('\Drupal\Core\Asset\LibraryDiscoveryParser::validateCssLibrary($library[$type]) < 2', 'CSS files should be specified as key/value pairs, where the values are configuration options. See https://www.drupal.org/node/2274843.');
- assert('\Drupal\Core\Asset\LibraryDiscoveryParser::validateCssLibrary($library[$type]) === 0', 'CSS must be nested under a category. See https://www.drupal.org/node/2274843.');
+ assert(static::validateCssLibrary($library[$type]) < 2, 'CSS files should be specified as key/value pairs, where the values are configuration options. See https://www.drupal.org/node/2274843.');
+ assert(static::validateCssLibrary($library[$type]) === 0, 'CSS must be nested under a category. See https://www.drupal.org/node/2274843.');
foreach ($library[$type] as $category => $files) {
$category_weight = 'CSS_' . strtoupper($category);
- assert('defined($category_weight)', 'Invalid CSS category: ' . $category . '. See https://www.drupal.org/node/2274843.');
+ assert(defined($category_weight), 'Invalid CSS category: ' . $category . '. See https://www.drupal.org/node/2274843.');
foreach ($files as $source => $options) {
if (!isset($options['weight'])) {
$options['weight'] = 0;
diff --git a/core/lib/Drupal/Core/Batch/BatchBuilder.php b/core/lib/Drupal/Core/Batch/BatchBuilder.php
new file mode 100644
index 0000000..dce6698
--- /dev/null
+++ b/core/lib/Drupal/Core/Batch/BatchBuilder.php
@@ -0,0 +1,340 @@
+<?php
+
+namespace Drupal\Core\Batch;
+
+use Drupal\Core\Queue\QueueInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+
+/**
+ * Builds an array for a batch process.
+ *
+ * Example code to create a batch:
+ * @code
+ * $batch_builder = (new BatchBuilder())
+ * ->setTitle(t('Batch Title'))
+ * ->setFinishCallback('batch_example_finished_callback')
+ * ->setInitMessage(t('The initialization message (optional)'));
+ * foreach ($ids as $id) {
+ * $batch_builder->addOperation('batch_example_callback', [$id]);
+ * }
+ * batch_set($batch_builder->toArray());
+ * @endcode
+ */
+class BatchBuilder {
+
+ /**
+ * The set of operations to be processed.
+ *
+ * Each operation is a tuple of the function / method to use and an array
+ * containing any parameters to be passed.
+ *
+ * @var array
+ */
+ protected $operations = [];
+
+ /**
+ * The title for the batch.
+ *
+ * @var string|\Drupal\Core\StringTranslation\TranslatableMarkup
+ */
+ protected $title;
+
+ /**
+ * The initializing message for the batch.
+ *
+ * @var string|\Drupal\Core\StringTranslation\TranslatableMarkup
+ */
+ protected $initMessage;
+
+ /**
+ * The message to be shown while the batch is in progress.
+ *
+ * @var string|\Drupal\Core\StringTranslation\TranslatableMarkup
+ */
+ protected $progressMessage;
+
+ /**
+ * The message to be shown if a problem occurs.
+ *
+ * @var string|\Drupal\Core\StringTranslation\TranslatableMarkup
+ */
+ protected $errorMessage;
+
+ /**
+ * The name of a function / method to be called when the batch finishes.
+ *
+ * @var string
+ */
+ protected $finished;
+
+ /**
+ * The file containing the operation and finished callbacks.
+ *
+ * If the callbacks are in the .module file or can be autoloaded, for example,
+ * static methods on a class, then this does not need to be set.
+ *
+ * @var string
+ */
+ protected $file;
+
+ /**
+ * An array of libraries to be included when processing the batch.
+ *
+ * @var string[]
+ */
+ protected $libraries = [];
+
+ /**
+ * An array of options to be used with the redirect URL.
+ *
+ * @var array
+ */
+ protected $urlOptions = [];
+
+ /**
+ * Specifies if the batch is progressive.
+ *
+ * If true, multiple calls are used. Otherwise an attempt is made to process
+ * the batch in a single run.
+ *
+ * @var bool
+ */
+ protected $progressive = TRUE;
+
+ /**
+ * The details of the queue to use.
+ *
+ * A tuple containing the name of the queue and the class of the queue to use.
+ *
+ * @var array
+ */
+ protected $queue;
+
+ /**
+ * Sets the default values for the batch builder.
+ */
+ public function __construct() {
+ $this->title = new TranslatableMarkup('Processing');
+ $this->initMessage = new TranslatableMarkup('Initializing.');
+ $this->progressMessage = new TranslatableMarkup('Completed @current of @total.');
+ $this->errorMessage = new TranslatableMarkup('An error has occurred.');
+ }
+
+ /**
+ * Sets the title.
+ *
+ * @param string|\Drupal\Core\StringTranslation\TranslatableMarkup $title
+ * The title.
+ *
+ * @return $this
+ */
+ public function setTitle($title) {
+ $this->title = $title;
+ return $this;
+ }
+
+ /**
+ * Sets the finished callback.
+ *
+ * This callback will be executed if the batch process is done.
+ *
+ * @param callable $callback
+ * The callback.
+ *
+ * @return $this
+ */
+ public function setFinishCallback(callable $callback) {
+ $this->finished = $callback;
+ return $this;
+ }
+
+ /**
+ * Sets the displayed message while processing is initialized.
+ *
+ * Defaults to 'Initializing.'.
+ *
+ * @param string|\Drupal\Core\StringTranslation\TranslatableMarkup $message
+ * The text to display.
+ *
+ * @return $this
+ */
+ public function setInitMessage($message) {
+ $this->initMessage = $message;
+ return $this;
+ }
+
+ /**
+ * Sets the message to display when the batch is being processed.
+ *
+ * Defaults to 'Completed @current of @total.'.
+ *
+ * @param string|\Drupal\Core\StringTranslation\TranslatableMarkup $message
+ * The text to display. Available placeholders are:
+ * - '@current'
+ * - '@remaining'
+ * - '@total'
+ * - '@percentage'
+ * - '@estimate'
+ * - '@elapsed'.
+ *
+ * @return $this
+ */
+ public function setProgressMessage($message) {
+ $this->progressMessage = $message;
+ return $this;
+ }
+
+ /**
+ * Sets the message to display if an error occurs while processing.
+ *
+ * Defaults to 'An error has occurred.'.
+ *
+ * @param string|\Drupal\Core\StringTranslation\TranslatableMarkup $message
+ * The text to display.
+ *
+ * @return $this
+ */
+ public function setErrorMessage($message) {
+ $this->errorMessage = $message;
+ return $this;
+ }
+
+ /**
+ * Sets the file that contains the callback functions.
+ *
+ * The path should be relative to base_path(), and thus should be built using
+ * drupal_get_path(). Defaults to {module_name}.module.
+ *
+ * @param string $filename
+ * The path to the file.
+ *
+ * @return $this
+ */
+ public function setFile($filename) {
+ $this->file = $filename;
+ return $this;
+ }
+
+ /**
+ * Sets the libraries to use when processing the batch.
+ *
+ * Adds the libraries for use on the progress page. Any previously added
+ * libraries are removed.
+ *
+ * @param string[] $libraries
+ * The libraries to be used.
+ *
+ * @return $this
+ */
+ public function setLibraries(array $libraries) {
+ $this->libraries = $libraries;
+ return $this;
+ }
+
+ /**
+ * Sets the options for redirect URLs.
+ *
+ * @param array $options
+ * The options to use.
+ *
+ * @return $this
+ *
+ * @see \Drupal\Core\Url
+ */
+ public function setUrlOptions(array $options) {
+ $this->urlOptions = $options;
+ return $this;
+ }
+
+ /**
+ * Sets the batch to run progressively.
+ *
+ * @param bool $is_progressive
+ * (optional) A Boolean that indicates whether or not the batch needs to run
+ * progressively. TRUE indicates that the batch will run in more than one
+ * run. FALSE indicates that the batch will finish in a single run. Defaults
+ * to TRUE.
+ *
+ * @return $this
+ */
+ public function setProgressive($is_progressive = TRUE) {
+ $this->progressive = $is_progressive;
+ return $this;
+ }
+
+ /**
+ * Sets an override for the default queue.
+ *
+ * The class will typically either be \Drupal\Core\Queue\Batch or
+ * \Drupal\Core\Queue\BatchMemory. The class defaults to Batch if progressive
+ * is TRUE, or to BatchMemory if progressive is FALSE.
+ *
+ * @param string $name
+ * The unique identifier for the queue.
+ * @param string $class
+ * The fully qualified name of a class that implements
+ * \Drupal\Core\Queue\QueueInterface.
+ *
+ * @return $this
+ */
+ public function setQueue($name, $class) {
+ if (!class_exists($class)) {
+ throw new \InvalidArgumentException('Class ' . $class . ' does not exist.');
+ }
+
+ if (!in_array(QueueInterface::class, class_implements($class))) {
+ throw new \InvalidArgumentException(
+ 'Class ' . $class . ' does not implement \Drupal\Core\Queue\QueueInterface.'
+ );
+ }
+
+ $this->queue = [
+ 'name' => $name,
+ 'class' => $class,
+ ];
+ return $this;
+ }
+
+ /**
+ * Adds a batch operation.
+ *
+ * @param callable $callback
+ * The name of the callback function.
+ * @param array $arguments
+ * An array of arguments to pass to the callback function.
+ *
+ * @return $this
+ */
+ public function addOperation(callable $callback, array $arguments = []) {
+ $this->operations[] = [$callback, $arguments];
+ return $this;
+ }
+
+ /**
+ * Converts a \Drupal\Core\Batch\Batch object into an array.
+ *
+ * @return array
+ * The array representation of the object.
+ */
+ public function toArray() {
+ $array = [
+ 'operations' => $this->operations ?: [],
+ 'title' => $this->title ?: '',
+ 'init_message' => $this->initMessage ?: '',
+ 'progress_message' => $this->progressMessage ?: '',
+ 'error_message' => $this->errorMessage ?: '',
+ 'finished' => $this->finished,
+ 'file' => $this->file,
+ 'library' => $this->libraries ?: [],
+ 'url_options' => $this->urlOptions ?: [],
+ 'progressive' => $this->progressive,
+ ];
+
+ if ($this->queue) {
+ $array['queue'] = $this->queue;
+ }
+
+ return $array;
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Block/BlockBase.php b/core/lib/Drupal/Core/Block/BlockBase.php
index ee90783..bebdf09 100644
--- a/core/lib/Drupal/Core/Block/BlockBase.php
+++ b/core/lib/Drupal/Core/Block/BlockBase.php
@@ -4,13 +4,14 @@ namespace Drupal\Core\Block;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Messenger\MessengerTrait;
use Drupal\Core\Plugin\ContextAwarePluginAssignmentTrait;
use Drupal\Core\Plugin\ContextAwarePluginBase;
-use Drupal\Component\Utility\Unicode;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Plugin\PluginWithFormsInterface;
use Drupal\Core\Plugin\PluginWithFormsTrait;
+use Drupal\Core\Render\PreviewFallbackInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Component\Transliteration\TransliterationInterface;
@@ -23,9 +24,10 @@ use Drupal\Component\Transliteration\TransliterationInterface;
*
* @ingroup block_api
*/
-abstract class BlockBase extends ContextAwarePluginBase implements BlockPluginInterface, PluginWithFormsInterface {
+abstract class BlockBase extends ContextAwarePluginBase implements BlockPluginInterface, PluginWithFormsInterface, PreviewFallbackInterface {
use ContextAwarePluginAssignmentTrait;
+ use MessengerTrait;
use PluginWithFormsTrait;
/**
@@ -244,7 +246,7 @@ abstract class BlockBase extends ContextAwarePluginBase implements BlockPluginIn
// \Drupal\system\MachineNameController::transliterate(), so it might make
// sense to provide a common service for the two.
$transliterated = $this->transliteration()->transliterate($admin_label, LanguageInterface::LANGCODE_DEFAULT, '_');
- $transliterated = Unicode::strtolower($transliterated);
+ $transliterated = mb_strtolower($transliterated);
$transliterated = preg_replace('@[^a-z0-9_.]+@', '', $transliterated);
@@ -252,6 +254,13 @@ abstract class BlockBase extends ContextAwarePluginBase implements BlockPluginIn
}
/**
+ * {@inheritdoc}
+ */
+ public function getPreviewFallbackString() {
+ return $this->t('Placeholder for the "@block" block', ['@block' => $this->label()]);
+ }
+
+ /**
* Wraps the transliteration service.
*
* @return \Drupal\Component\Transliteration\TransliterationInterface
diff --git a/core/lib/Drupal/Core/Block/BlockManager.php b/core/lib/Drupal/Core/Block/BlockManager.php
index 30b52b6..026d810 100644
--- a/core/lib/Drupal/Core/Block/BlockManager.php
+++ b/core/lib/Drupal/Core/Block/BlockManager.php
@@ -6,8 +6,9 @@ use Drupal\Component\Plugin\FallbackPluginManagerInterface;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\CategorizingPluginManagerTrait;
-use Drupal\Core\Plugin\Context\ContextAwarePluginManagerTrait;
use Drupal\Core\Plugin\DefaultPluginManager;
+use Drupal\Core\Plugin\FilteredPluginManagerTrait;
+use Psr\Log\LoggerInterface;
/**
* Manages discovery and instantiation of block plugins.
@@ -21,7 +22,14 @@ class BlockManager extends DefaultPluginManager implements BlockManagerInterface
use CategorizingPluginManagerTrait {
getSortedDefinitions as traitGetSortedDefinitions;
}
- use ContextAwarePluginManagerTrait;
+ use FilteredPluginManagerTrait;
+
+ /**
+ * The logger.
+ *
+ * @var \Psr\Log\LoggerInterface
+ */
+ protected $logger;
/**
* Constructs a new \Drupal\Core\Block\BlockManager object.
@@ -33,12 +41,22 @@ class BlockManager extends DefaultPluginManager implements BlockManagerInterface
* Cache backend instance to use.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler to invoke the alter hook with.
+ * @param \Psr\Log\LoggerInterface $logger
+ * The logger.
*/
- public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
+ public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler, LoggerInterface $logger) {
parent::__construct('Plugin/Block', $namespaces, $module_handler, 'Drupal\Core\Block\BlockPluginInterface', 'Drupal\Core\Block\Annotation\Block');
- $this->alterInfo('block');
+ $this->alterInfo($this->getType());
$this->setCacheBackend($cache_backend, 'block_plugins');
+ $this->logger = $logger;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getType() {
+ return 'block';
}
/**
@@ -67,4 +85,12 @@ class BlockManager extends DefaultPluginManager implements BlockManagerInterface
return 'broken';
}
+ /**
+ * {@inheritdoc}
+ */
+ protected function handlePluginNotFound($plugin_id, array $configuration) {
+ $this->logger->warning('The "%plugin_id" was not found', ['%plugin_id' => $plugin_id]);
+ return parent::handlePluginNotFound($plugin_id, $configuration);
+ }
+
}
diff --git a/core/lib/Drupal/Core/Block/BlockManagerInterface.php b/core/lib/Drupal/Core/Block/BlockManagerInterface.php
index 3455f23..7b5d5c6 100644
--- a/core/lib/Drupal/Core/Block/BlockManagerInterface.php
+++ b/core/lib/Drupal/Core/Block/BlockManagerInterface.php
@@ -4,10 +4,11 @@ namespace Drupal\Core\Block;
use Drupal\Component\Plugin\CategorizingPluginManagerInterface;
use Drupal\Core\Plugin\Context\ContextAwarePluginManagerInterface;
+use Drupal\Core\Plugin\FilteredPluginManagerInterface;
/**
* Provides an interface for the discovery and instantiation of block plugins.
*/
-interface BlockManagerInterface extends ContextAwarePluginManagerInterface, CategorizingPluginManagerInterface {
+interface BlockManagerInterface extends ContextAwarePluginManagerInterface, CategorizingPluginManagerInterface, FilteredPluginManagerInterface {
}
diff --git a/core/lib/Drupal/Core/Block/MessagesBlockPluginInterface.php b/core/lib/Drupal/Core/Block/MessagesBlockPluginInterface.php
index 995f4b7..9248ac1 100644
--- a/core/lib/Drupal/Core/Block/MessagesBlockPluginInterface.php
+++ b/core/lib/Drupal/Core/Block/MessagesBlockPluginInterface.php
@@ -5,8 +5,7 @@ namespace Drupal\Core\Block;
/**
* The interface for "messages" (#type => status_messages) blocks.
*
- * @see drupal_set_message()
- * @see drupal_get_message()
+ * @see \Drupal\Core\Messenger\MessengerInterface
* @see \Drupal\Core\Render\Element\StatusMessages
* @see \Drupal\block\Plugin\DisplayVariant\BlockPageVariant
*
diff --git a/core/lib/Drupal/Core/Block/Plugin/Block/Broken.php b/core/lib/Drupal/Core/Block/Plugin/Block/Broken.php
index 0e4732e..b7f3a19 100644
--- a/core/lib/Drupal/Core/Block/Plugin/Block/Broken.php
+++ b/core/lib/Drupal/Core/Block/Plugin/Block/Broken.php
@@ -38,7 +38,7 @@ class Broken extends BlockBase {
*/
protected function brokenMessage() {
$build['message'] = [
- '#markup' => $this->t('This block is broken or missing. You may be missing content or you might need to enable the original module.')
+ '#markup' => $this->t('This block is broken or missing. You may be missing content or you might need to enable the original module.'),
];
return $build;
diff --git a/core/lib/Drupal/Core/Block/Plugin/Block/PageTitleBlock.php b/core/lib/Drupal/Core/Block/Plugin/Block/PageTitleBlock.php
index 4589973..af1fb6e 100644
--- a/core/lib/Drupal/Core/Block/Plugin/Block/PageTitleBlock.php
+++ b/core/lib/Drupal/Core/Block/Plugin/Block/PageTitleBlock.php
@@ -11,6 +11,9 @@ use Drupal\Core\Block\TitleBlockPluginInterface;
* @Block(
* id = "page_title_block",
* admin_label = @Translation("Page title"),
+ * forms = {
+ * "settings_tray" = FALSE,
+ * },
* )
*/
class PageTitleBlock extends BlockBase implements TitleBlockPluginInterface {
diff --git a/core/lib/Drupal/Core/Cache/ApcuBackend.php b/core/lib/Drupal/Core/Cache/ApcuBackend.php
index d7847ff..e04e014 100644
--- a/core/lib/Drupal/Core/Cache/ApcuBackend.php
+++ b/core/lib/Drupal/Core/Cache/ApcuBackend.php
@@ -2,6 +2,8 @@
namespace Drupal\Core\Cache;
+use Drupal\Component\Assertion\Inspector;
+
/**
* Stores cache items in the Alternative PHP Cache User Cache (APCu).
*/
@@ -161,7 +163,7 @@ class ApcuBackend implements CacheBackendInterface {
* {@inheritdoc}
*/
public function set($cid, $data, $expire = CacheBackendInterface::CACHE_PERMANENT, array $tags = []) {
- assert('\Drupal\Component\Assertion\Inspector::assertAllStrings($tags)', 'Cache tags must be strings.');
+ assert(Inspector::assertAllStrings($tags), 'Cache tags must be strings.');
$tags = array_unique($tags);
$cache = new \stdClass();
$cache->cid = $cid;
diff --git a/core/lib/Drupal/Core/Cache/Cache.php b/core/lib/Drupal/Core/Cache/Cache.php
index 9257aab..53aa181 100644
--- a/core/lib/Drupal/Core/Cache/Cache.php
+++ b/core/lib/Drupal/Core/Cache/Cache.php
@@ -2,6 +2,7 @@
namespace Drupal\Core\Cache;
+use Drupal\Component\Assertion\Inspector;
use Drupal\Core\Database\Query\SelectInterface;
/**
@@ -29,7 +30,7 @@ class Cache {
*/
public static function mergeContexts(array $a = [], array $b = []) {
$cache_contexts = array_unique(array_merge($a, $b));
- assert('\Drupal::service(\'cache_contexts_manager\')->assertValidTokens($cache_contexts)');
+ assert(\Drupal::service('cache_contexts_manager')->assertValidTokens($cache_contexts));
sort($cache_contexts);
return $cache_contexts;
}
@@ -54,7 +55,7 @@ class Cache {
* The merged array of cache tags.
*/
public static function mergeTags(array $a = [], array $b = []) {
- assert('\Drupal\Component\Assertion\Inspector::assertAllStrings($a) && \Drupal\Component\Assertion\Inspector::assertAllStrings($b)', 'Cache tags must be valid strings');
+ assert(Inspector::assertAllStrings($a) && Inspector::assertAllStrings($b), 'Cache tags must be valid strings');
$cache_tags = array_unique(array_merge($a, $b));
sort($cache_tags);
@@ -96,7 +97,7 @@ class Cache {
* An array of cache tags.
*
* @deprecated
- * Use assert('\Drupal\Component\Assertion\Inspector::assertAllStrings($tags)');
+ * Use assert(Inspector::assertAllStrings($tags));
*
* @throws \LogicException
*/
diff --git a/core/lib/Drupal/Core/Cache/CacheBackendInterface.php b/core/lib/Drupal/Core/Cache/CacheBackendInterface.php
index 852305f..bf2ed7c 100644
--- a/core/lib/Drupal/Core/Cache/CacheBackendInterface.php
+++ b/core/lib/Drupal/Core/Cache/CacheBackendInterface.php
@@ -9,7 +9,7 @@ namespace Drupal\Core\Cache;
* Drupal\Core\Cache\DatabaseBackend provides the default implementation, which
* can be consulted as an example.
*
- * The cache indentifiers are case sensitive.
+ * The cache identifiers are case sensitive.
*
* @ingroup cache
*/
diff --git a/core/lib/Drupal/Core/Cache/CacheCollector.php b/core/lib/Drupal/Core/Cache/CacheCollector.php
index b0c9f45..912b60f 100644
--- a/core/lib/Drupal/Core/Cache/CacheCollector.php
+++ b/core/lib/Drupal/Core/Cache/CacheCollector.php
@@ -2,6 +2,7 @@
namespace Drupal\Core\Cache;
+use Drupal\Component\Assertion\Inspector;
use Drupal\Component\Utility\Crypt;
use Drupal\Core\DestructableInterface;
use Drupal\Core\Lock\LockBackendInterface;
@@ -111,7 +112,7 @@ abstract class CacheCollector implements CacheCollectorInterface, DestructableIn
* (optional) The tags to specify for the cache item.
*/
public function __construct($cid, CacheBackendInterface $cache, LockBackendInterface $lock, array $tags = []) {
- assert('\Drupal\Component\Assertion\Inspector::assertAllStrings($tags)', 'Cache tags must be strings.');
+ assert(Inspector::assertAllStrings($tags), 'Cache tags must be strings.');
$this->cid = $cid;
$this->cache = $cache;
$this->tags = $tags;
diff --git a/core/lib/Drupal/Core/Cache/CacheTagsInvalidator.php b/core/lib/Drupal/Core/Cache/CacheTagsInvalidator.php
index ceb8f49..820da98 100644
--- a/core/lib/Drupal/Core/Cache/CacheTagsInvalidator.php
+++ b/core/lib/Drupal/Core/Cache/CacheTagsInvalidator.php
@@ -2,6 +2,7 @@
namespace Drupal\Core\Cache;
+use Drupal\Component\Assertion\Inspector;
use Symfony\Component\DependencyInjection\ContainerAwareTrait;
/**
@@ -22,7 +23,7 @@ class CacheTagsInvalidator implements CacheTagsInvalidatorInterface {
* {@inheritdoc}
*/
public function invalidateTags(array $tags) {
- assert('Drupal\Component\Assertion\Inspector::assertAllStrings($tags)', 'Cache tags must be strings.');
+ assert(Inspector::assertAllStrings($tags), 'Cache tags must be strings.');
// Notify all added cache tags invalidators.
foreach ($this->invalidators as $invalidator) {
diff --git a/core/lib/Drupal/Core/Cache/Context/CacheContextsManager.php b/core/lib/Drupal/Core/Cache/Context/CacheContextsManager.php
index 9a555f8..056f838 100644
--- a/core/lib/Drupal/Core/Cache/Context/CacheContextsManager.php
+++ b/core/lib/Drupal/Core/Cache/Context/CacheContextsManager.php
@@ -100,7 +100,7 @@ class CacheContextsManager {
* cacheability metadata.
*/
public function convertTokensToKeys(array $context_tokens) {
- assert('$this->assertValidTokens($context_tokens)');
+ assert($this->assertValidTokens($context_tokens));
$cacheable_metadata = new CacheableMetadata();
$optimized_tokens = $this->optimizeTokens($context_tokens);
// Iterate over cache contexts that have been optimized away and get their
diff --git a/core/lib/Drupal/Core/Cache/Context/LanguagesCacheContext.php b/core/lib/Drupal/Core/Cache/Context/LanguagesCacheContext.php
index 952dc14..620787b 100644
--- a/core/lib/Drupal/Core/Cache/Context/LanguagesCacheContext.php
+++ b/core/lib/Drupal/Core/Cache/Context/LanguagesCacheContext.php
@@ -13,7 +13,7 @@ class LanguagesCacheContext implements CalculatedCacheContextInterface {
/**
* The language manager.
*
- * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
+ * @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
diff --git a/core/lib/Drupal/Core/Cache/Context/SiteCacheContext.php b/core/lib/Drupal/Core/Cache/Context/SiteCacheContext.php
index 5c9a937..16516c5 100644
--- a/core/lib/Drupal/Core/Cache/Context/SiteCacheContext.php
+++ b/core/lib/Drupal/Core/Cache/Context/SiteCacheContext.php
@@ -7,7 +7,7 @@ use Drupal\Core\Cache\CacheableMetadata;
/**
* Defines the SiteCacheContext service, for "per site" caching.
*
- * Cache context ID: 'site'.
+ * Cache context ID: 'url.site'.
*
* A "site" is defined as the combination of URI scheme, domain name, port and
* base path. It allows for varying between the *same* site being accessed via
diff --git a/core/lib/Drupal/Core/Cache/DatabaseBackend.php b/core/lib/Drupal/Core/Cache/DatabaseBackend.php
index 747186b..88f018f 100644
--- a/core/lib/Drupal/Core/Cache/DatabaseBackend.php
+++ b/core/lib/Drupal/Core/Cache/DatabaseBackend.php
@@ -2,6 +2,7 @@
namespace Drupal\Core\Cache;
+use Drupal\Component\Assertion\Inspector;
use Drupal\Component\Utility\Crypt;
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\SchemaObjectExistsException;
@@ -222,7 +223,7 @@ class DatabaseBackend implements CacheBackendInterface {
'tags' => [],
];
- assert('\Drupal\Component\Assertion\Inspector::assertAllStrings($item[\'tags\'])', 'Cache Tags must be strings.');
+ assert(Inspector::assertAllStrings($item['tags']), 'Cache Tags must be strings.');
$item['tags'] = array_unique($item['tags']);
// Sort the cache tags so that they are stored consistently in the DB.
sort($item['tags']);
diff --git a/core/lib/Drupal/Core/Cache/DatabaseBackendFactory.php b/core/lib/Drupal/Core/Cache/DatabaseBackendFactory.php
index b86390e..6bde00b 100644
--- a/core/lib/Drupal/Core/Cache/DatabaseBackendFactory.php
+++ b/core/lib/Drupal/Core/Cache/DatabaseBackendFactory.php
@@ -74,7 +74,7 @@ class DatabaseBackendFactory implements CacheFactoryInterface {
$max_rows_settings = $this->settings->get('database_cache_max_rows');
// First, look for a cache bin specific setting.
if (isset($max_rows_settings['bins'][$bin])) {
- $max_rows = $max_rows_settings['bins'][$bin];
+ $max_rows = $max_rows_settings['bins'][$bin];
}
// Second, use configured default backend.
elseif (isset($max_rows_settings['default'])) {
diff --git a/core/lib/Drupal/Core/Cache/MemoryBackend.php b/core/lib/Drupal/Core/Cache/MemoryBackend.php
index edda4d3..c127692 100644
--- a/core/lib/Drupal/Core/Cache/MemoryBackend.php
+++ b/core/lib/Drupal/Core/Cache/MemoryBackend.php
@@ -2,6 +2,8 @@
namespace Drupal\Core\Cache;
+use Drupal\Component\Assertion\Inspector;
+
/**
* Defines a memory cache implementation.
*
@@ -98,7 +100,7 @@ class MemoryBackend implements CacheBackendInterface, CacheTagsInvalidatorInterf
* {@inheritdoc}
*/
public function set($cid, $data, $expire = Cache::PERMANENT, array $tags = []) {
- assert('\Drupal\Component\Assertion\Inspector::assertAllStrings($tags)', 'Cache Tags must be strings.');
+ assert(Inspector::assertAllStrings($tags), 'Cache Tags must be strings.');
$tags = array_unique($tags);
// Sort the cache tags so that they are stored consistently in the database.
sort($tags);
@@ -154,10 +156,9 @@ class MemoryBackend implements CacheBackendInterface, CacheTagsInvalidatorInterf
* {@inheritdoc}
*/
public function invalidateMultiple(array $cids) {
- foreach ($cids as $cid) {
- if (isset($this->cache[$cid])) {
- $this->cache[$cid]->expire = $this->getRequestTime() - 1;
- }
+ $items = array_intersect_key($this->cache, array_flip($cids));
+ foreach ($items as $cid => $item) {
+ $this->cache[$cid]->expire = $this->getRequestTime() - 1;
}
}
diff --git a/core/lib/Drupal/Core/Cache/MemoryCache/MemoryCache.php b/core/lib/Drupal/Core/Cache/MemoryCache/MemoryCache.php
new file mode 100644
index 0000000..433edd2
--- /dev/null
+++ b/core/lib/Drupal/Core/Cache/MemoryCache/MemoryCache.php
@@ -0,0 +1,63 @@
+<?php
+
+namespace Drupal\Core\Cache\MemoryCache;
+
+use Drupal\Component\Assertion\Inspector;
+use Drupal\Core\Cache\MemoryBackend;
+
+/**
+ * Defines a memory cache implementation.
+ *
+ * Stores cache items in memory using a PHP array.
+ *
+ * @ingroup cache
+ */
+class MemoryCache extends MemoryBackend implements MemoryCacheInterface {
+
+ /**
+ * Prepares a cached item.
+ *
+ * Checks that items are either permanent or did not expire, and returns data
+ * as appropriate.
+ *
+ * @param object $cache
+ * An item loaded from cache_get() or cache_get_multiple().
+ * @param bool $allow_invalid
+ * (optional) If TRUE, cache items may be returned even if they have expired
+ * or been invalidated. Defaults to FALSE.
+ *
+ * @return mixed
+ * The item with data as appropriate or FALSE if there is no
+ * valid item to load.
+ */
+ protected function prepareItem($cache, $allow_invalid = FALSE) {
+ if (!isset($cache->data)) {
+ return FALSE;
+ }
+ // Check expire time.
+ $cache->valid = $cache->expire == static::CACHE_PERMANENT || $cache->expire >= $this->getRequestTime();
+
+ if (!$allow_invalid && !$cache->valid) {
+ return FALSE;
+ }
+
+ return $cache;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function set($cid, $data, $expire = MemoryCacheInterface::CACHE_PERMANENT, array $tags = []) {
+ assert(Inspector::assertAllStrings($tags), 'Cache tags must be strings.');
+ $tags = array_unique($tags);
+
+ $this->cache[$cid] = (object) [
+ 'cid' => $cid,
+ 'data' => $data,
+ 'created' => $this->getRequestTime(),
+ 'expire' => $expire,
+ 'tags' => $tags,
+ ];
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Cache/MemoryCache/MemoryCacheInterface.php b/core/lib/Drupal/Core/Cache/MemoryCache/MemoryCacheInterface.php
new file mode 100644
index 0000000..c794ea1
--- /dev/null
+++ b/core/lib/Drupal/Core/Cache/MemoryCache/MemoryCacheInterface.php
@@ -0,0 +1,18 @@
+<?php
+
+namespace Drupal\Core\Cache\MemoryCache;
+
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Cache\CacheTagsInvalidatorInterface;
+
+/**
+ * Defines an interface for memory cache implementations.
+ *
+ * This has additional requirements over CacheBackendInterface and
+ * CacheTagsInvalidatorInterface. Objects stored must be the same instance when
+ * retrieved from cache, so that this can be used as a replacement for protected
+ * properties and similar.
+ *
+ * @ingroup cache
+ */
+interface MemoryCacheInterface extends CacheBackendInterface, CacheTagsInvalidatorInterface {}
diff --git a/core/lib/Drupal/Core/Cache/PhpBackend.php b/core/lib/Drupal/Core/Cache/PhpBackend.php
index 2404493..d3af7f5 100644
--- a/core/lib/Drupal/Core/Cache/PhpBackend.php
+++ b/core/lib/Drupal/Core/Cache/PhpBackend.php
@@ -2,6 +2,7 @@
namespace Drupal\Core\Cache;
+use Drupal\Component\Assertion\Inspector;
use Drupal\Core\PhpStorage\PhpStorageFactory;
use Drupal\Component\Utility\Crypt;
@@ -143,7 +144,8 @@ class PhpBackend implements CacheBackendInterface {
* {@inheritdoc}
*/
public function set($cid, $data, $expire = Cache::PERMANENT, array $tags = []) {
- assert('\Drupal\Component\Assertion\Inspector::assertAllStrings($tags)', 'Cache Tags must be strings.');
+ assert(Inspector::assertAllStrings($tags), 'Cache Tags must be strings.');
+
$item = (object) [
'cid' => $cid,
'data' => $data,
diff --git a/core/lib/Drupal/Core/Command/DbDumpCommand.php b/core/lib/Drupal/Core/Command/DbDumpCommand.php
index aa81af0..a7d6d09 100644
--- a/core/lib/Drupal/Core/Command/DbDumpCommand.php
+++ b/core/lib/Drupal/Core/Command/DbDumpCommand.php
@@ -145,7 +145,7 @@ class DbDumpCommand extends DbCommandBase {
$name = $row['Field'];
// Parse out the field type and meta information.
preg_match('@([a-z]+)(?:\((\d+)(?:,(\d+))?\))?\s*(unsigned)?@', $row['Type'], $matches);
- $type = $this->fieldTypeMap($connection, $matches[1]);
+ $type = $this->fieldTypeMap($connection, $matches[1]);
if ($row['Extra'] === 'auto_increment') {
// If this is an auto increment, then the type is 'serial'.
$type = 'serial';
@@ -162,12 +162,19 @@ class DbDumpCommand extends DbCommandBase {
$definition['fields'][$name]['precision'] = $matches[2];
$definition['fields'][$name]['scale'] = $matches[3];
}
- elseif ($type === 'time' || $type === 'datetime') {
+ elseif ($type === 'time') {
// @todo Core doesn't support these, but copied from `migrate-db.sh` for now.
// Convert to varchar.
$definition['fields'][$name]['type'] = 'varchar';
$definition['fields'][$name]['length'] = '100';
}
+ elseif ($type === 'datetime') {
+ // Adjust for other database types.
+ $definition['fields'][$name]['mysql_type'] = 'datetime';
+ $definition['fields'][$name]['pgsql_type'] = 'timestamp without time zone';
+ $definition['fields'][$name]['sqlite_type'] = 'varchar';
+ $definition['fields'][$name]['sqlsrv_type'] = 'smalldatetime';
+ }
elseif (!isset($definition['fields'][$name]['size'])) {
// Try use the provided length, if it doesn't exist default to 100. It's
// not great but good enough for our dumps at this point.
@@ -259,8 +266,12 @@ class DbDumpCommand extends DbCommandBase {
$query = $connection->query("SHOW TABLE STATUS LIKE '{" . $table . "}'");
$data = $query->fetchAssoc();
+ // Map the collation to a character set. For example, 'utf8mb4_general_ci'
+ // (MySQL 5) or 'utf8mb4_0900_ai_ci' (MySQL 8) will be mapped to 'utf8mb4'.
+ list($charset,) = explode('_', $data['Collation'], 2);
+
// Set `mysql_character_set`. This will be ignored by other backends.
- $definition['mysql_character_set'] = str_replace('_general_ci', '', $data['Collation']);
+ $definition['mysql_character_set'] = $charset;
}
/**
diff --git a/core/lib/Drupal/Core/Command/InstallCommand.php b/core/lib/Drupal/Core/Command/InstallCommand.php
new file mode 100644
index 0000000..bff4bc4
--- /dev/null
+++ b/core/lib/Drupal/Core/Command/InstallCommand.php
@@ -0,0 +1,337 @@
+<?php
+
+namespace Drupal\Core\Command;
+
+use Drupal\Component\Utility\Crypt;
+use Drupal\Core\Database\ConnectionNotDefinedException;
+use Drupal\Core\Database\Database;
+use Drupal\Core\DrupalKernel;
+use Drupal\Core\Extension\ExtensionDiscovery;
+use Drupal\Core\Extension\InfoParserDynamic;
+use Drupal\Core\Site\Settings;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Style\SymfonyStyle;
+
+/**
+ * Installs a Drupal site for local testing/development.
+ *
+ * @internal
+ * This command makes no guarantee of an API for Drupal extensions.
+ */
+class InstallCommand extends Command {
+
+ /**
+ * The class loader.
+ *
+ * @var object
+ */
+ protected $classLoader;
+
+ /**
+ * Constructs a new InstallCommand command.
+ *
+ * @param object $class_loader
+ * The class loader.
+ */
+ public function __construct($class_loader) {
+ parent::__construct('install');
+ $this->classLoader = $class_loader;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function configure() {
+ $this->setName('install')
+ ->setDescription('Installs a Drupal demo site. This is not meant for production and might be too simple for custom development. It is a quick and easy way to get Drupal running.')
+ ->addArgument('install-profile', InputArgument::OPTIONAL, 'Install profile to install the site in.')
+ ->addOption('langcode', NULL, InputOption::VALUE_OPTIONAL, 'The language to install the site in.', 'en')
+ ->addOption('site-name', NULL, InputOption::VALUE_OPTIONAL, 'Set the site name.', 'Drupal')
+ ->addUsage('demo_umami --langcode fr')
+ ->addUsage('standard --site-name QuickInstall');
+
+ parent::configure();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function execute(InputInterface $input, OutputInterface $output) {
+ $io = new SymfonyStyle($input, $output);
+ if (!extension_loaded('pdo_sqlite')) {
+ $io->getErrorStyle()->error('You must have the pdo_sqlite PHP extension installed. See core/INSTALL.sqlite.txt for instructions.');
+ return 1;
+ }
+
+ // Change the directory to the Drupal root.
+ chdir(dirname(dirname(dirname(dirname(dirname(__DIR__))))));
+
+ // Check whether there is already an installation.
+ if ($this->isDrupalInstalled()) {
+ // Do not fail if the site is already installed so this command can be
+ // chained with ServerCommand.
+ $output->writeln('<info>Drupal is already installed.</info> If you want to reinstall, remove sites/default/files and sites/default/settings.php.');
+ return 0;
+ }
+
+ $install_profile = $input->getArgument('install-profile');
+ if ($install_profile && !$this->validateProfile($install_profile, $io)) {
+ return 1;
+ }
+ if (!$install_profile) {
+ $install_profile = $this->selectProfile($io);
+ }
+
+ return $this->install($this->classLoader, $io, $install_profile, $input->getOption('langcode'), $this->getSitePath(), $input->getOption('site-name'));
+ }
+
+ /**
+ * Returns whether there is already an existing Drupal installation.
+ *
+ * @return bool
+ */
+ protected function isDrupalInstalled() {
+ try {
+ $kernel = new DrupalKernel('prod', $this->classLoader, FALSE);
+ $kernel::bootEnvironment();
+ $kernel->setSitePath($this->getSitePath());
+ Settings::initialize($kernel->getAppRoot(), $kernel->getSitePath(), $this->classLoader);
+ $kernel->boot();
+ }
+ catch (ConnectionNotDefinedException $e) {
+ return FALSE;
+ }
+ return !empty(Database::getConnectionInfo());
+ }
+
+ /**
+ * Installs Drupal with specified installation profile.
+ *
+ * @param object $class_loader
+ * The class loader.
+ * @param \Symfony\Component\Console\Style\SymfonyStyle $io
+ * The Symfony output decorator.
+ * @param string $profile
+ * The installation profile to use.
+ * @param string $langcode
+ * The language to install the site in.
+ * @param string $site_path
+ * The path to install the site to, like 'sites/default'.
+ * @param string $site_name
+ * The site name.
+ *
+ * @throws \Exception
+ * Thrown when failing to create the $site_path directory or settings.php.
+ */
+ protected function install($class_loader, SymfonyStyle $io, $profile, $langcode, $site_path, $site_name) {
+ $password = Crypt::randomBytesBase64(12);
+ $parameters = [
+ 'interactive' => FALSE,
+ 'site_path' => $site_path,
+ 'parameters' => [
+ 'profile' => $profile,
+ 'langcode' => $langcode,
+ ],
+ 'forms' => [
+ 'install_settings_form' => [
+ 'driver' => 'sqlite',
+ 'sqlite' => [
+ 'database' => $site_path . '/files/.sqlite',
+ ],
+ ],
+ 'install_configure_form' => [
+ 'site_name' => $site_name,
+ 'site_mail' => 'drupal@localhost',
+ 'account' => [
+ 'name' => 'admin',
+ 'mail' => 'admin@localhost',
+ 'pass' => [
+ 'pass1' => $password,
+ 'pass2' => $password,
+ ],
+ ],
+ 'enable_update_status_module' => TRUE,
+ // form_type_checkboxes_value() requires NULL instead of FALSE values
+ // for programmatic form submissions to disable a checkbox.
+ 'enable_update_status_emails' => NULL,
+ ],
+ ],
+ ];
+
+ // Create the directory and settings.php if not there so that the installer
+ // works.
+ if (!is_dir($site_path)) {
+ if ($io->isVerbose()) {
+ $io->writeln("Creating directory: $site_path");
+ }
+ if (!mkdir($site_path, 0775)) {
+ throw new \RuntimeException("Failed to create directory $site_path");
+ }
+ }
+ if (!file_exists("{$site_path}/settings.php")) {
+ if ($io->isVerbose()) {
+ $io->writeln("Creating file: {$site_path}/settings.php");
+ }
+ if (!copy('sites/default/default.settings.php', "{$site_path}/settings.php")) {
+ throw new \RuntimeException("Copying sites/default/default.settings.php to {$site_path}/settings.php failed.");
+ }
+ }
+
+ require_once 'core/includes/install.core.inc';
+
+ $progress_bar = $io->createProgressBar();
+ install_drupal($class_loader, $parameters, function ($install_state) use ($progress_bar) {
+ static $started = FALSE;
+ if (!$started) {
+ $started = TRUE;
+ // We've already done 1.
+ $progress_bar->setFormat("%current%/%max% [%bar%]\n%message%\n");
+ $progress_bar->setMessage(t('Installing @drupal', ['@drupal' => drupal_install_profile_distribution_name()]));
+ $tasks = install_tasks($install_state);
+ $progress_bar->start(count($tasks) + 1);
+ }
+ $tasks_to_perform = install_tasks_to_perform($install_state);
+ $task = current($tasks_to_perform);
+ if (isset($task['display_name'])) {
+ $progress_bar->setMessage($task['display_name']);
+ }
+ $progress_bar->advance();
+ });
+ $success_message = t('Congratulations, you installed @drupal!', [
+ '@drupal' => drupal_install_profile_distribution_name(),
+ '@name' => 'admin',
+ '@pass' => $password,
+ ], ['langcode' => $langcode]);
+ $progress_bar->setMessage('<info>' . $success_message . '</info>');
+ $progress_bar->display();
+ $progress_bar->finish();
+ $io->writeln('<info>Username:</info> admin');
+ $io->writeln("<info>Password:</info> $password");
+ }
+
+ /**
+ * Gets the site path.
+ *
+ * Defaults to 'sites/default'. For testing purposes this can be overridden
+ * using the DRUPAL_DEV_SITE_PATH environment variable.
+ *
+ * @return string
+ * The site path to use.
+ */
+ protected function getSitePath() {
+ return getenv('DRUPAL_DEV_SITE_PATH') ?: 'sites/default';
+ }
+
+ /**
+ * Selects the install profile to use.
+ *
+ * @param \Symfony\Component\Console\Style\SymfonyStyle $io
+ * Symfony style output decorator.
+ *
+ * @return string
+ * The selected install profile.
+ *
+ * @see _install_select_profile()
+ * @see \Drupal\Core\Installer\Form\SelectProfileForm
+ */
+ protected function selectProfile(SymfonyStyle $io) {
+ $profiles = $this->getProfiles();
+
+ // If there is a distribution there will be only one profile.
+ if (count($profiles) == 1) {
+ return key($profiles);
+ }
+ // Display alphabetically by human-readable name, but always put the core
+ // profiles first (if they are present in the filesystem).
+ natcasesort($profiles);
+ if (isset($profiles['minimal'])) {
+ // If the expert ("Minimal") core profile is present, put it in front of
+ // any non-core profiles rather than including it with them
+ // alphabetically, since the other profiles might be intended to group
+ // together in a particular way.
+ $profiles = ['minimal' => $profiles['minimal']] + $profiles;
+ }
+ if (isset($profiles['standard'])) {
+ // If the default ("Standard") core profile is present, put it at the very
+ // top of the list. This profile will have its radio button pre-selected,
+ // so we want it to always appear at the top.
+ $profiles = ['standard' => $profiles['standard']] + $profiles;
+ }
+ reset($profiles);
+ return $io->choice('Select an installation profile', $profiles, current($profiles));
+ }
+
+ /**
+ * Validates a user provided install profile.
+ *
+ * @param string $install_profile
+ * Install profile to validate.
+ * @param \Symfony\Component\Console\Style\SymfonyStyle $io
+ * Symfony style output decorator.
+ *
+ * @return bool
+ * TRUE if the profile is valid, FALSE if not.
+ */
+ protected function validateProfile($install_profile, SymfonyStyle $io) {
+ // Allow people to install hidden and non-distribution profiles if they
+ // supply the argument.
+ $profiles = $this->getProfiles(TRUE, FALSE);
+ if (!isset($profiles[$install_profile])) {
+ $error_msg = sprintf("'%s' is not a valid install profile.", $install_profile);
+ $alternatives = [];
+ foreach (array_keys($profiles) as $profile_name) {
+ $lev = levenshtein($install_profile, $profile_name);
+ if ($lev <= strlen($profile_name) / 4 || FALSE !== strpos($profile_name, $install_profile)) {
+ $alternatives[] = $profile_name;
+ }
+ }
+ if (!empty($alternatives)) {
+ $error_msg .= sprintf(" Did you mean '%s'?", implode("' or '", $alternatives));
+ }
+ $io->getErrorStyle()->error($error_msg);
+ return FALSE;
+ }
+ return TRUE;
+ }
+
+ /**
+ * Gets a list of profiles.
+ *
+ * @param bool $include_hidden
+ * (optional) Whether to include hidden profiles. Defaults to FALSE.
+ * @param bool $auto_select_distributions
+ * (optional) Whether to only return the first distribution found.
+ *
+ * @return string[]
+ * An array of profile descriptions keyed by the profile machine name.
+ */
+ protected function getProfiles($include_hidden = FALSE, $auto_select_distributions = TRUE) {
+ // Build a list of all available profiles.
+ $listing = new ExtensionDiscovery(getcwd(), FALSE);
+ $listing->setProfileDirectories([]);
+ $profiles = [];
+ $info_parser = new InfoParserDynamic();
+ foreach ($listing->scan('profile') as $profile) {
+ $details = $info_parser->parse($profile->getPathname());
+ // Don't show hidden profiles.
+ if (!$include_hidden && !empty($details['hidden'])) {
+ continue;
+ }
+ // Determine the name of the profile; default to the internal name if none
+ // is specified.
+ $name = isset($details['name']) ? $details['name'] : $profile->getName();
+ $description = isset($details['description']) ? $details['description'] : $name;
+ $profiles[$profile->getName()] = $description;
+
+ if ($auto_select_distributions && !empty($details['distribution'])) {
+ return [$profile->getName() => $description];
+ }
+ }
+ return $profiles;
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Command/QuickStartCommand.php b/core/lib/Drupal/Core/Command/QuickStartCommand.php
new file mode 100644
index 0000000..690b20e
--- /dev/null
+++ b/core/lib/Drupal/Core/Command/QuickStartCommand.php
@@ -0,0 +1,76 @@
+<?php
+
+namespace Drupal\Core\Command;
+
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\ArrayInput;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+/**
+ * Installs a Drupal site and starts a webserver for local testing/development.
+ *
+ * Wraps 'install' and 'server' commands.
+ *
+ * @internal
+ * This command makes no guarantee of an API for Drupal extensions.
+ *
+ * @see \Drupal\Core\Command\InstallCommand
+ * @see \Drupal\Core\Command\ServerCommand
+ */
+class QuickStartCommand extends Command {
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function configure() {
+ $this->setName('quick-start')
+ ->setDescription('Installs a Drupal site and runs a web server. This is not meant for production and might be too simple for custom development. It is a quick and easy way to get Drupal running.')
+ ->addArgument('install-profile', InputArgument::OPTIONAL, 'Install profile to install the site in.')
+ ->addOption('langcode', NULL, InputOption::VALUE_OPTIONAL, 'The language to install the site in. Defaults to en.', 'en')
+ ->addOption('site-name', NULL, InputOption::VALUE_OPTIONAL, 'Set the site name. Defaults to Drupal.', 'Drupal')
+ ->addOption('host', NULL, InputOption::VALUE_OPTIONAL, 'Provide a host for the server to run on. Defaults to 127.0.0.1.', '127.0.0.1')
+ ->addOption('port', NULL, InputOption::VALUE_OPTIONAL, 'Provide a port for the server to run on. Will be determined automatically if none supplied.')
+ ->addOption('suppress-login', 's', InputOption::VALUE_NONE, 'Disable opening a login URL in a browser.')
+ ->addUsage('demo_umami --langcode fr')
+ ->addUsage('standard --site-name QuickInstall --host localhost --port 8080')
+ ->addUsage('minimal --host my-site.com --port 80');
+
+ parent::configure();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function execute(InputInterface $input, OutputInterface $output) {
+ $command = $this->getApplication()->find('install');
+
+ $arguments = [
+ 'command' => 'install',
+ 'install-profile' => $input->getArgument('install-profile'),
+ '--langcode' => $input->getOption('langcode'),
+ '--site-name' => $input->getOption('site-name'),
+ ];
+
+ $installInput = new ArrayInput($arguments);
+ $returnCode = $command->run($installInput, $output);
+
+ if ($returnCode === 0) {
+ $command = $this->getApplication()->find('server');
+ $arguments = [
+ 'command' => 'server',
+ '--host' => $input->getOption('host'),
+ '--port' => $input->getOption('port'),
+ ];
+ if ($input->getOption('suppress-login')) {
+ $arguments['--suppress-login'] = TRUE;
+ }
+ $serverInput = new ArrayInput($arguments);
+ $returnCode = $command->run($serverInput, $output);
+ }
+ return $returnCode;
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Command/ServerCommand.php b/core/lib/Drupal/Core/Command/ServerCommand.php
new file mode 100644
index 0000000..182167a
--- /dev/null
+++ b/core/lib/Drupal/Core/Command/ServerCommand.php
@@ -0,0 +1,278 @@
+<?php
+
+namespace Drupal\Core\Command;
+
+use Drupal\Core\Database\ConnectionNotDefinedException;
+use Drupal\Core\DrupalKernel;
+use Drupal\Core\DrupalKernelInterface;
+use Drupal\Core\Site\Settings;
+use Drupal\user\Entity\User;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Style\SymfonyStyle;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\Process\PhpExecutableFinder;
+use Symfony\Component\Process\PhpProcess;
+use Symfony\Component\Process\Process;
+
+/**
+ * Runs the PHP webserver for a Drupal site for local testing/development.
+ *
+ * @internal
+ * This command makes no guarantee of an API for Drupal extensions.
+ */
+class ServerCommand extends Command {
+
+ /**
+ * The class loader.
+ *
+ * @var object
+ */
+ protected $classLoader;
+
+ /**
+ * Constructs a new ServerCommand command.
+ *
+ * @param object $class_loader
+ * The class loader.
+ */
+ public function __construct($class_loader) {
+ parent::__construct('server');
+ $this->classLoader = $class_loader;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function configure() {
+ $this->setDescription('Starts up a webserver for a site.')
+ ->addOption('host', NULL, InputOption::VALUE_OPTIONAL, 'Provide a host for the server to run on.', '127.0.0.1')
+ ->addOption('port', NULL, InputOption::VALUE_OPTIONAL, 'Provide a port for the server to run on. Will be determined automatically if none supplied.')
+ ->addOption('suppress-login', 's', InputOption::VALUE_NONE, 'Disable opening a login URL in a browser.')
+ ->addUsage('--host localhost --port 8080')
+ ->addUsage('--host my-site.com --port 80');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function execute(InputInterface $input, OutputInterface $output) {
+ $io = new SymfonyStyle($input, $output);
+
+ $host = $input->getOption('host');
+ $port = $input->getOption('port');
+ if (!$port) {
+ $port = $this->findAvailablePort($host);
+ }
+ if (!$port) {
+ $io->getErrorStyle()->error('Unable to automatically determine a port. Use the --port to hardcode an available port.');
+ }
+
+ try {
+ $kernel = $this->boot();
+ }
+ catch (ConnectionNotDefinedException $e) {
+ $io->getErrorStyle()->error("No installation found. Use the 'install' command.");
+ return 1;
+ }
+ return $this->start($host, $port, $kernel, $input, $io);
+ }
+
+ /**
+ * Boots up a Drupal environment.
+ *
+ * @return \Drupal\Core\DrupalKernelInterface
+ * The Drupal kernel.
+ *
+ * @throws \Exception
+ * Exception thrown if kernel does not boot.
+ */
+ protected function boot() {
+ $kernel = new DrupalKernel('prod', $this->classLoader, FALSE);
+ $kernel::bootEnvironment();
+ $kernel->setSitePath($this->getSitePath());
+ Settings::initialize($kernel->getAppRoot(), $kernel->getSitePath(), $this->classLoader);
+ $kernel->boot();
+ // Some services require a request to work. For example, CommentManager.
+ // This is needed as generating the URL fires up entity load hooks.
+ $kernel->getContainer()
+ ->get('request_stack')
+ ->push(Request::createFromGlobals());
+
+ return $kernel;
+ }
+
+ /**
+ * Finds an available port.
+ *
+ * @param string $host
+ * The host to find a port on.
+ *
+ * @return int|false
+ * The available port or FALSE, if no available port found,
+ */
+ protected function findAvailablePort($host) {
+ $port = 8888;
+ while ($port >= 8888 && $port <= 9999) {
+ $connection = @fsockopen($host, $port);
+ if (is_resource($connection)) {
+ // Port is being used.
+ fclose($connection);
+ }
+ else {
+ // Port is available.
+ return $port;
+ }
+ $port++;
+ }
+ return FALSE;
+ }
+
+ /**
+ * Opens a URL in your system default browser.
+ *
+ * @param string $url
+ * The URL to browser to.
+ * @param \Symfony\Component\Console\Style\SymfonyStyle $io
+ * The IO.
+ */
+ protected function openBrowser($url, SymfonyStyle $io) {
+ $is_windows = defined('PHP_WINDOWS_VERSION_BUILD');
+ if ($is_windows) {
+ // Handle escaping ourselves.
+ $cmd = 'start "web" "' . $url . '""';
+ }
+ else {
+ $url = escapeshellarg($url);
+ }
+
+ $is_linux = (new Process('which xdg-open'))->run();
+ $is_osx = (new Process('which open'))->run();
+ if ($is_linux === 0) {
+ $cmd = 'xdg-open ' . $url;
+ }
+ elseif ($is_osx === 0) {
+ $cmd = 'open ' . $url;
+ }
+
+ if (empty($cmd)) {
+ $io->getErrorStyle()
+ ->error('No suitable browser opening command found, open yourself: ' . $url);
+ return;
+ }
+
+ if ($io->isVerbose()) {
+ $io->writeln("<info>Browser command:</info> $cmd");
+ }
+
+ // Need to escape double quotes in the command so the PHP will work.
+ $cmd = str_replace('"', '\"', $cmd);
+ // Sleep for 2 seconds before opening the browser. This allows the command
+ // to start up the PHP built-in webserver in the meantime. We use a
+ // PhpProcess so that Windows powershell users also get a browser opened
+ // for them.
+ $php = "<?php sleep(2); passthru(\"$cmd\"); ?>";
+ $process = new PhpProcess($php);
+ $process->start();
+ return;
+ }
+
+ /**
+ * Gets a one time login URL for user 1.
+ *
+ * @return string
+ * The one time login URL for user 1.
+ */
+ protected function getOneTimeLoginUrl() {
+ $user = User::load(1);
+ \Drupal::moduleHandler()->load('user');
+ return user_pass_reset_url($user);
+ }
+
+ /**
+ * Starts up a webserver with a running Drupal.
+ *
+ * @param string $host
+ * The hostname of the webserver.
+ * @param int $port
+ * The port to start the webserver on.
+ * @param \Drupal\Core\DrupalKernelInterface $kernel
+ * The Drupal kernel.
+ * @param \Symfony\Component\Console\Input\InputInterface $input
+ * The input.
+ * @param \Symfony\Component\Console\Style\SymfonyStyle $io
+ * The IO.
+ *
+ * @return int
+ * The exit status of the PHP in-built webserver command.
+ */
+ protected function start($host, $port, DrupalKernelInterface $kernel, InputInterface $input, SymfonyStyle $io) {
+ $finder = new PhpExecutableFinder();
+ $binary = $finder->find();
+ if ($binary === FALSE) {
+ throw new \RuntimeException('Unable to find the PHP binary.');
+ }
+
+ $io->writeln("<info>Drupal development server started:</info> <http://{$host}:{$port}>");
+ $io->writeln('<info>This server is not meant for production use.</info>');
+ $one_time_login = "http://$host:$port{$this->getOneTimeLoginUrl()}/login";
+ $io->writeln("<info>One time login url:</info> <$one_time_login>");
+ $io->writeln('Press Ctrl-C to quit the Drupal development server.');
+
+ if (!$input->getOption('suppress-login')) {
+ if ($this->openBrowser("$one_time_login?destination=" . urlencode("/"), $io) === 1) {
+ $io->error('Error while opening up a one time login URL');
+ }
+ }
+
+ // Use the Process object to construct an escaped command line.
+ $process = new Process([
+ $binary,
+ '-S',
+ $host . ':' . $port,
+ '.ht.router.php',
+ ], $kernel->getAppRoot(), [], NULL, NULL);
+ if ($io->isVerbose()) {
+ $io->writeln("<info>Server command:</info> {$process->getCommandLine()}");
+ }
+
+ // Carefully manage output so we can display output only in verbose mode.
+ $descriptors = [];
+ $descriptors[0] = STDIN;
+ $descriptors[1] = ['pipe', 'w'];
+ $descriptors[2] = ['pipe', 'w'];
+ $server = proc_open($process->getCommandLine(), $descriptors, $pipes, $kernel->getAppRoot());
+ if (is_resource($server)) {
+ if ($io->isVerbose()) {
+ // Write a blank line so that server output and the useful information are
+ // visually separated.
+ $io->writeln('');
+ }
+ $server_status = proc_get_status($server);
+ while ($server_status['running']) {
+ if ($io->isVerbose()) {
+ fpassthru($pipes[2]);
+ }
+ sleep(1);
+ $server_status = proc_get_status($server);
+ }
+ }
+ return proc_close($server);
+ }
+
+ /**
+ * Gets the site path.
+ *
+ * Defaults to 'sites/default'. For testing purposes this can be overridden
+ * using the DRUPAL_DEV_SITE_PATH environment variable.
+ *
+ * @return string
+ * The site path to use.
+ */
+ protected function getSitePath() {
+ return getenv('DRUPAL_DEV_SITE_PATH') ?: 'sites/default';
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Composer/Composer.php b/core/lib/Drupal/Core/Composer/Composer.php
index 016f93a..9833b7e 100644
--- a/core/lib/Drupal/Core/Composer/Composer.php
+++ b/core/lib/Drupal/Core/Composer/Composer.php
@@ -6,6 +6,7 @@ use Drupal\Component\PhpStorage\FileStorage;
use Composer\Script\Event;
use Composer\Installer\PackageEvent;
use Composer\Semver\Constraint\Constraint;
+use Composer\Util\ProcessExecutor;
/**
* Provides static functions for composer script events.
@@ -144,6 +145,48 @@ EOT;
}
/**
+ * Fires the drupal-phpunit-upgrade script event if necessary.
+ *
+ * @param \Composer\Script\Event $event
+ */
+ public static function upgradePHPUnit(Event $event) {
+ $repository = $event->getComposer()->getRepositoryManager()->getLocalRepository();
+ // This is, essentially, a null constraint. We only care whether the package
+ // is present in the vendor directory yet, but findPackage() requires it.
+ $constraint = new Constraint('>', '');
+ $phpunit_package = $repository->findPackage('phpunit/phpunit', $constraint);
+ if (!$phpunit_package) {
+ // There is nothing to do. The user is probably installing using the
+ // --no-dev flag.
+ return;
+ }
+
+ // If the PHP version is 7.0 or above and PHPUnit is less than version 6
+ // call the drupal-phpunit-upgrade script to upgrade PHPUnit.
+ if (!static::upgradePHPUnitCheck($phpunit_package->getVersion())) {
+ $event->getComposer()
+ ->getEventDispatcher()
+ ->dispatchScript('drupal-phpunit-upgrade');
+ }
+ }
+
+ /**
+ * Determines if PHPUnit needs to be upgraded.
+ *
+ * This method is located in this file because it is possible that it is
+ * called before the autoloader is available.
+ *
+ * @param string $phpunit_version
+ * The PHPUnit version string.
+ *
+ * @return bool
+ * TRUE if the PHPUnit needs to be upgraded, FALSE if not.
+ */
+ public static function upgradePHPUnitCheck($phpunit_version) {
+ return !(version_compare(PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION, '7.0') >= 0 && version_compare($phpunit_version, '6.1') < 0);
+ }
+
+ /**
* Remove possibly problematic test files from vendored projects.
*
* @param \Composer\Installer\PackageEvent $event
@@ -228,6 +271,13 @@ EOT;
}
/**
+ * Removes Composer's timeout so that scripts can run indefinitely.
+ */
+ public static function removeTimeout() {
+ ProcessExecutor::setTimeout(0);
+ }
+
+ /**
* Helper method to remove directories and the files they contain.
*
* @param string $path
diff --git a/core/lib/Drupal/Core/Condition/ConditionManager.php b/core/lib/Drupal/Core/Condition/ConditionManager.php
index 2c6b61b..89fe216 100644
--- a/core/lib/Drupal/Core/Condition/ConditionManager.php
+++ b/core/lib/Drupal/Core/Condition/ConditionManager.php
@@ -8,8 +8,9 @@ use Drupal\Core\Executable\ExecutableManagerInterface;
use Drupal\Core\Executable\ExecutableInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\CategorizingPluginManagerTrait;
-use Drupal\Core\Plugin\Context\ContextAwarePluginManagerTrait;
use Drupal\Core\Plugin\DefaultPluginManager;
+use Drupal\Core\Plugin\FilteredPluginManagerInterface;
+use Drupal\Core\Plugin\FilteredPluginManagerTrait;
/**
* A plugin manager for condition plugins.
@@ -20,10 +21,10 @@ use Drupal\Core\Plugin\DefaultPluginManager;
*
* @ingroup plugin_api
*/
-class ConditionManager extends DefaultPluginManager implements ExecutableManagerInterface, CategorizingPluginManagerInterface {
+class ConditionManager extends DefaultPluginManager implements ExecutableManagerInterface, CategorizingPluginManagerInterface, FilteredPluginManagerInterface {
use CategorizingPluginManagerTrait;
- use ContextAwarePluginManagerTrait;
+ use FilteredPluginManagerTrait;
/**
* Constructs a ConditionManager object.
@@ -46,6 +47,13 @@ class ConditionManager extends DefaultPluginManager implements ExecutableManager
/**
* {@inheritdoc}
*/
+ protected function getType() {
+ return 'condition';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
public function createInstance($plugin_id, array $configuration = []) {
$plugin = $this->getFactory()->createInstance($plugin_id, $configuration);
diff --git a/core/lib/Drupal/Core/Config/Config.php b/core/lib/Drupal/Core/Config/Config.php
index 584feb7..f7e0ede 100644
--- a/core/lib/Drupal/Core/Config/Config.php
+++ b/core/lib/Drupal/Core/Config/Config.php
@@ -219,6 +219,10 @@ class Config extends StorableConfigBase {
}
}
+ // Potentially configuration schema could have changed the underlying data's
+ // types.
+ $this->resetOverriddenData();
+
$this->storage->write($this->name, $this->data);
if (!$this->isNew) {
Cache::invalidateTags($this->getCacheTags());
@@ -226,9 +230,6 @@ class Config extends StorableConfigBase {
$this->isNew = FALSE;
$this->eventDispatcher->dispatch(ConfigEvents::SAVE, new ConfigCrudEvent($this));
$this->originalData = $this->data;
- // Potentially configuration schema could have changed the underlying data's
- // types.
- $this->resetOverriddenData();
return $this;
}
@@ -303,4 +304,42 @@ class Config extends StorableConfigBase {
}
}
+ /**
+ * Determines if overrides are applied to a key for this configuration object.
+ *
+ * @param string $key
+ * (optional) A string that maps to a key within the configuration data.
+ * For instance in the following configuration array:
+ * @code
+ * array(
+ * 'foo' => array(
+ * 'bar' => 'baz',
+ * ),
+ * );
+ * @endcode
+ * A key of 'foo.bar' would map to the string 'baz'. However, a key of 'foo'
+ * would map to the array('bar' => 'baz').
+ * If not supplied TRUE will be returned if there are any overrides at all
+ * for this configuration object.
+ *
+ * @return bool
+ * TRUE if there are any overrides for the key, otherwise FALSE.
+ */
+ public function hasOverrides($key = '') {
+ if (empty($key)) {
+ return !(empty($this->moduleOverrides) && empty($this->settingsOverrides));
+ }
+ else {
+ $parts = explode('.', $key);
+ $override_exists = FALSE;
+ if (isset($this->moduleOverrides) && is_array($this->moduleOverrides)) {
+ $override_exists = NestedArray::keyExists($this->moduleOverrides, $parts);
+ }
+ if (!$override_exists && isset($this->settingsOverrides) && is_array($this->settingsOverrides)) {
+ $override_exists = NestedArray::keyExists($this->settingsOverrides, $parts);
+ }
+ return $override_exists;
+ }
+ }
+
}
diff --git a/core/lib/Drupal/Core/Config/ConfigImporter.php b/core/lib/Drupal/Core/Config/ConfigImporter.php
index 06fed4b..0f9eae9 100644
--- a/core/lib/Drupal/Core/Config/ConfigImporter.php
+++ b/core/lib/Drupal/Core/Config/ConfigImporter.php
@@ -405,6 +405,14 @@ class ConfigImporter {
$module_list = array_reverse($module_list);
$this->extensionChangelist['module']['install'] = array_intersect(array_keys($module_list), $install);
+ // If we're installing the install profile ensure it comes last. This will
+ // occur when installing a site from configuration.
+ $install_profile_key = array_search($new_extensions['profile'], $this->extensionChangelist['module']['install'], TRUE);
+ if ($install_profile_key !== FALSE) {
+ unset($this->extensionChangelist['module']['install'][$install_profile_key]);
+ $this->extensionChangelist['module']['install'][] = $new_extensions['profile'];
+ }
+
// Work out what themes to install and to uninstall.
$this->extensionChangelist['theme']['install'] = array_keys(array_diff_key($new_extensions['theme'], $current_extensions['theme']));
$this->extensionChangelist['theme']['uninstall'] = array_keys(array_diff_key($current_extensions['theme'], $new_extensions['theme']));
@@ -725,7 +733,8 @@ class ConfigImporter {
}
$this->eventDispatcher->dispatch(ConfigEvents::IMPORT_VALIDATE, new ConfigImporterEvent($this));
if (count($this->getErrors())) {
- throw new ConfigImporterException('There were errors validating the config synchronization.');
+ $errors = array_merge(['There were errors validating the config synchronization.'], $this->getErrors());
+ throw new ConfigImporterException(implode(PHP_EOL, $errors));
}
else {
$this->validated = TRUE;
@@ -788,9 +797,8 @@ class ConfigImporter {
// services.
$this->reInjectMe();
// During a module install or uninstall the container is rebuilt and the
- // module handler is called from drupal_get_complete_schema(). This causes
- // the container's instance of the module handler not to have loaded all
- // the enabled modules.
+ // module handler is called. This causes the container's instance of the
+ // module handler not to have loaded all the enabled modules.
$this->moduleHandler->loadAll();
}
if ($type == 'theme') {
diff --git a/core/lib/Drupal/Core/Config/ConfigInstaller.php b/core/lib/Drupal/Core/Config/ConfigInstaller.php
index 242d920..4a198a7 100644
--- a/core/lib/Drupal/Core/Config/ConfigInstaller.php
+++ b/core/lib/Drupal/Core/Config/ConfigInstaller.php
@@ -3,9 +3,7 @@
namespace Drupal\Core\Config;
use Drupal\Component\Utility\Crypt;
-use Drupal\Component\Utility\Unicode;
use Drupal\Core\Config\Entity\ConfigDependencyManager;
-use Drupal\Core\Config\Entity\ConfigEntityDependency;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
class ConfigInstaller implements ConfigInstallerInterface {
@@ -162,7 +160,10 @@ class ConfigInstaller implements ConfigInstallerInterface {
*/
public function installOptionalConfig(StorageInterface $storage = NULL, $dependency = []) {
$profile = $this->drupalGetProfile();
- $optional_profile_config = [];
+ $enabled_extensions = $this->getEnabledExtensions();
+ $existing_config = $this->getActiveStorages()->listAll();
+
+ // Create the storages to read configuration from.
if (!$storage) {
// Search the install profile's optional configuration too.
$storage = new ExtensionInstallStorage($this->getActiveStorages(StorageInterface::DEFAULT_COLLECTION), InstallStorage::CONFIG_OPTIONAL_DIRECTORY, StorageInterface::DEFAULT_COLLECTION, TRUE, $this->installProfile);
@@ -173,7 +174,6 @@ class ConfigInstaller implements ConfigInstallerInterface {
// Creates a profile storage to search for overrides.
$profile_install_path = $this->drupalGetPath('module', $profile) . '/' . InstallStorage::CONFIG_OPTIONAL_DIRECTORY;
$profile_storage = new FileStorage($profile_install_path, StorageInterface::DEFAULT_COLLECTION);
- $optional_profile_config = $profile_storage->listAll();
}
else {
// Profile has not been set yet. For example during the first steps of the
@@ -181,10 +181,17 @@ class ConfigInstaller implements ConfigInstallerInterface {
$profile_storage = NULL;
}
- $enabled_extensions = $this->getEnabledExtensions();
- $existing_config = $this->getActiveStorages()->listAll();
+ // Build the list of possible configuration to create.
+ $list = $storage->listAll();
+ if ($profile_storage && !empty($dependency)) {
+ // Only add the optional profile configuration into the list if we are
+ // have a dependency to check. This ensures that optional profile
+ // configuration is not unexpectedly re-created after being deleted.
+ $list = array_unique(array_merge($list, $profile_storage->listAll()));
+ }
- $list = array_unique(array_merge($storage->listAll(), $optional_profile_config));
+ // Filter the list of configuration to only include configuration that
+ // should be created.
$list = array_filter($list, function ($config_name) use ($existing_config) {
// Only list configuration that:
// - does not already exist
@@ -205,16 +212,19 @@ class ConfigInstaller implements ConfigInstallerInterface {
$dependency_manager = new ConfigDependencyManager();
$dependency_manager->setData($config_to_create);
$config_to_create = array_merge(array_flip($dependency_manager->sortAll()), $config_to_create);
+ if (!empty($dependency)) {
+ // In order to work out dependencies we need the full config graph.
+ $dependency_manager->setData($this->getActiveStorages()->readMultiple($existing_config) + $config_to_create);
+ $dependencies = $dependency_manager->getDependentEntities(key($dependency), reset($dependency));
+ }
foreach ($config_to_create as $config_name => $data) {
// Remove configuration where its dependencies cannot be met.
$remove = !$this->validateDependencies($config_name, $data, $enabled_extensions, $all_config);
- // If $dependency is defined, remove configuration that does not have a
- // matching dependency.
+ // Remove configuration that is not dependent on $dependency, if it is
+ // defined.
if (!$remove && !empty($dependency)) {
- // Create a light weight dependency object to check dependencies.
- $config_entity = new ConfigEntityDependency($config_name, $data);
- $remove = !$config_entity->hasDependency(key($dependency), reset($dependency));
+ $remove = !isset($dependencies[$config_name]);
}
if ($remove) {
@@ -225,6 +235,8 @@ class ConfigInstaller implements ConfigInstallerInterface {
unset($all_config[$config_name]);
}
}
+
+ // Create the optional configuration if there is any left after filtering.
if (!empty($config_to_create)) {
$this->createConfiguration(StorageInterface::DEFAULT_COLLECTION, $config_to_create, TRUE);
}
@@ -316,10 +328,11 @@ class ConfigInstaller implements ConfigInstallerInterface {
$entity_storage = $this->configManager
->getEntityManager()
->getStorage($entity_type);
+
+ $id = $entity_storage->getIDFromConfigName($name, $entity_storage->getEntityType()->getConfigPrefix());
// It is possible that secondary writes can occur during configuration
// creation. Updates of such configuration are allowed.
if ($this->getActiveStorages($collection)->exists($name)) {
- $id = $entity_storage->getIDFromConfigName($name, $entity_storage->getEntityType()->getConfigPrefix());
$entity = $entity_storage->load($id);
$entity = $entity_storage->updateFromStorageRecord($entity, $new_config->get());
}
@@ -328,6 +341,9 @@ class ConfigInstaller implements ConfigInstallerInterface {
}
if ($entity->isInstallable()) {
$entity->trustData()->save();
+ if ($id !== $entity->id()) {
+ trigger_error(sprintf('The configuration name "%s" does not match the ID "%s"', $name, $entity->id()), E_USER_WARNING);
+ }
}
}
else {
@@ -344,7 +360,7 @@ class ConfigInstaller implements ConfigInstallerInterface {
// Only install configuration for enabled extensions.
$enabled_extensions = $this->getEnabledExtensions();
$config_to_install = array_filter($storage->listAll(), function ($config_name) use ($enabled_extensions) {
- $provider = Unicode::substr($config_name, 0, strpos($config_name, '.'));
+ $provider = mb_substr($config_name, 0, strpos($config_name, '.'));
return in_array($provider, $enabled_extensions);
});
if (!empty($config_to_install)) {
diff --git a/core/lib/Drupal/Core/Config/ConfigInstallerInterface.php b/core/lib/Drupal/Core/Config/ConfigInstallerInterface.php
index 3d7077b..41b015e 100644
--- a/core/lib/Drupal/Core/Config/ConfigInstallerInterface.php
+++ b/core/lib/Drupal/Core/Config/ConfigInstallerInterface.php
@@ -43,7 +43,8 @@ interface ConfigInstallerInterface {
* @param \Drupal\Core\Config\StorageInterface $storage
* (optional) The configuration storage to search for optional
* configuration. If not provided, all enabled extension's optional
- * configuration directories will be searched.
+ * configuration directories including the install profile's will be
+ * searched.
* @param array $dependency
* (optional) If set, ensures that the configuration being installed has
* this dependency. The format is dependency type as the key ('module',
diff --git a/core/lib/Drupal/Core/Config/ConfigManager.php b/core/lib/Drupal/Core/Config/ConfigManager.php
index 8b3b3d6..e766683 100644
--- a/core/lib/Drupal/Core/Config/ConfigManager.php
+++ b/core/lib/Drupal/Core/Config/ConfigManager.php
@@ -292,78 +292,85 @@ class ConfigManager implements ConfigManagerInterface {
* {@inheritdoc}
*/
public function getConfigEntitiesToChangeOnDependencyRemoval($type, array $names, $dry_run = TRUE) {
- // Determine the current list of dependent configuration entities and set up
- // initial values.
$dependency_manager = $this->getConfigDependencyManager();
- $dependents = $this->findConfigEntityDependentsAsEntities($type, $names, $dependency_manager);
- $original_dependencies = $dependents;
- $delete_uuids = [];
+ // Store the list of dependents in three separate variables. This allows us
+ // to determine how the dependency graph changes as entities are fixed by
+ // calling the onDependencyRemoval() method.
+
+ // The list of original dependents on $names. This list never changes.
+ $original_dependents = $this->findConfigEntityDependentsAsEntities($type, $names, $dependency_manager);
+
+ // The current list of dependents on $names. This list is recalculated when
+ // calling an entity's onDependencyRemoval() method results in the entity
+ // changing. This list is passed to each entity's onDependencyRemoval()
+ // method as the list of affected entities.
+ $current_dependents = $original_dependents;
+
+ // The list of dependents to process. This list changes as entities are
+ // processed and are either fixed or deleted.
+ $dependents_to_process = $original_dependents;
+
+ // Initialize other variables.
+ $affected_uuids = [];
$return = [
'update' => [],
'delete' => [],
'unchanged' => [],
];
- // Create a map of UUIDs to $original_dependencies key so that we can remove
- // fixed dependencies.
- $uuid_map = [];
- foreach ($original_dependencies as $key => $entity) {
- $uuid_map[$entity->uuid()] = $key;
- }
-
- // Try to fix any dependencies and find out what will happen to the
- // dependency graph. Entities are processed in the order of most dependent
- // first. For example, this ensures that Menu UI third party dependencies on
- // node types are fixed before processing the node type's other
- // dependencies.
- while ($dependent = array_pop($dependents)) {
+ // Try to fix the dependents and find out what will happen to the dependency
+ // graph. Entities are processed in the order of most dependent first. For
+ // example, this ensures that Menu UI third party dependencies on node types
+ // are fixed before processing the node type's other dependents.
+ while ($dependent = array_pop($dependents_to_process)) {
/** @var \Drupal\Core\Config\Entity\ConfigEntityInterface $dependent */
if ($dry_run) {
// Clone the entity so any changes do not change any static caches.
$dependent = clone $dependent;
}
$fixed = FALSE;
- if ($this->callOnDependencyRemoval($dependent, $original_dependencies, $type, $names)) {
+ if ($this->callOnDependencyRemoval($dependent, $current_dependents, $type, $names)) {
// Recalculate dependencies and update the dependency graph data.
$dependent->calculateDependencies();
$dependency_manager->updateData($dependent->getConfigDependencyName(), $dependent->getDependencies());
- // Based on the updated data rebuild the list of dependents. This will
- // remove entities that are no longer dependent after the recalculation.
- $dependents = $this->findConfigEntityDependentsAsEntities($type, $names, $dependency_manager);
- // Remove any entities that we've already marked for deletion.
- $dependents = array_filter($dependents, function ($dependent) use ($delete_uuids) {
- return !in_array($dependent->uuid(), $delete_uuids);
+ // Based on the updated data rebuild the list of current dependents.
+ // This will remove entities that are no longer dependent after the
+ // recalculation.
+ $current_dependents = $this->findConfigEntityDependentsAsEntities($type, $names, $dependency_manager);
+ // Rebuild the list of entities that we need to process using the new
+ // list of current dependents and removing any entities that we've
+ // already processed.
+ $dependents_to_process = array_filter($current_dependents, function ($current_dependent) use ($affected_uuids) {
+ return !in_array($current_dependent->uuid(), $affected_uuids);
});
- // Ensure that the dependency has actually been fixed. It is possible
- // that the dependent has multiple dependencies that cause it to be in
- // the dependency chain.
+ // Ensure that the dependent has actually been fixed. It is possible
+ // that other dependencies cause it to still be in the list.
$fixed = TRUE;
- foreach ($dependents as $key => $entity) {
+ foreach ($dependents_to_process as $key => $entity) {
if ($entity->uuid() == $dependent->uuid()) {
$fixed = FALSE;
- unset($dependents[$key]);
+ unset($dependents_to_process[$key]);
break;
}
}
if ($fixed) {
- // Remove the fixed dependency from the list of original dependencies.
- unset($original_dependencies[$uuid_map[$dependent->uuid()]]);
+ $affected_uuids[] = $dependent->uuid();
$return['update'][] = $dependent;
}
}
// If the entity cannot be fixed then it has to be deleted.
if (!$fixed) {
- $delete_uuids[] = $dependent->uuid();
+ $affected_uuids[] = $dependent->uuid();
// Deletes should occur in the order of the least dependent first. For
// example, this ensures that fields are removed before field storages.
array_unshift($return['delete'], $dependent);
}
}
- // Use the lists of UUIDs to filter the original list to work out which
- // configuration entities are unchanged.
- $return['unchanged'] = array_filter($original_dependencies, function ($dependent) use ($delete_uuids) {
- return !(in_array($dependent->uuid(), $delete_uuids));
+ // Use the list of affected UUIDs to filter the original list to work out
+ // which configuration entities are unchanged.
+ $return['unchanged'] = array_filter($original_dependents, function ($dependent) use ($affected_uuids) {
+ return !(in_array($dependent->uuid(), $affected_uuids));
});
return $return;
diff --git a/core/lib/Drupal/Core/Config/DatabaseStorage.php b/core/lib/Drupal/Core/Config/DatabaseStorage.php
index 0ac6134..2560a46 100644
--- a/core/lib/Drupal/Core/Config/DatabaseStorage.php
+++ b/core/lib/Drupal/Core/Config/DatabaseStorage.php
@@ -228,7 +228,6 @@ class DatabaseStorage implements StorageInterface {
->execute();
}
-
/**
* Implements Drupal\Core\Config\StorageInterface::rename().
*
diff --git a/core/lib/Drupal/Core/Config/Development/ConfigSchemaChecker.php b/core/lib/Drupal/Core/Config/Development/ConfigSchemaChecker.php
index ba69001..0ca1004 100644
--- a/core/lib/Drupal/Core/Config/Development/ConfigSchemaChecker.php
+++ b/core/lib/Drupal/Core/Config/Development/ConfigSchemaChecker.php
@@ -3,7 +3,7 @@
namespace Drupal\Core\Config\Development;
use Drupal\Component\Utility\Crypt;
-use Drupal\Component\Utility\SafeMarkup;
+use Drupal\Component\Render\FormattableMarkup;
use Drupal\Core\Config\ConfigCrudEvent;
use Drupal\Core\Config\ConfigEvents;
use Drupal\Core\Config\Schema\SchemaCheckTrait;
@@ -90,7 +90,7 @@ class ConfigSchemaChecker implements EventSubscriberInterface {
elseif (is_array($errors)) {
$text_errors = [];
foreach ($errors as $key => $error) {
- $text_errors[] = SafeMarkup::format('@key @error', ['@key' => $key, '@error' => $error]);
+ $text_errors[] = new FormattableMarkup('@key @error', ['@key' => $key, '@error' => $error]);
}
throw new SchemaIncompleteException("Schema errors for $name with the following errors: " . implode(', ', $text_errors));
}
diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php b/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php
index 90bb702..4efcade 100644
--- a/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php
+++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php
@@ -267,18 +267,13 @@ abstract class ConfigEntityBase extends Entity implements ConfigEntityInterface
/** @var \Drupal\Core\Config\Entity\ConfigEntityTypeInterface $entity_type */
$entity_type = $this->getEntityType();
- $properties_to_export = $entity_type->getPropertiesToExport();
- if (empty($properties_to_export)) {
+ $id_key = $entity_type->getKey('id');
+ $property_names = $entity_type->getPropertiesToExport($this->id());
+ if (empty($property_names)) {
$config_name = $entity_type->getConfigPrefix() . '.' . $this->id();
- $definition = $this->getTypedConfig()->getDefinition($config_name);
- if (!isset($definition['mapping'])) {
- throw new SchemaIncompleteException("Incomplete or missing schema for $config_name");
- }
- $properties_to_export = array_combine(array_keys($definition['mapping']), array_keys($definition['mapping']));
+ throw new SchemaIncompleteException("Incomplete or missing schema for $config_name");
}
-
- $id_key = $entity_type->getKey('id');
- foreach ($properties_to_export as $property_name => $export_name) {
+ foreach ($property_names as $property_name => $export_name) {
// Special handling for IDs so that computed compound IDs work.
// @see \Drupal\Core\Entity\EntityDisplayBase::id()
if ($property_name == $id_key) {
diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigEntityInterface.php b/core/lib/Drupal/Core/Config/Entity/ConfigEntityInterface.php
index 248701e..8931f1e 100644
--- a/core/lib/Drupal/Core/Config/Entity/ConfigEntityInterface.php
+++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityInterface.php
@@ -189,7 +189,7 @@ interface ConfigEntityInterface extends EntityInterface, ThirdPartySettingsInter
* For example, a default view might not be installable if the base table
* doesn't exist.
*
- * @retun bool
+ * @return bool
* TRUE if the entity is installable, FALSE otherwise.
*/
public function isInstallable();
diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigEntityStorage.php b/core/lib/Drupal/Core/Config/Entity/ConfigEntityStorage.php
index 62fb13e..257fdf0 100644
--- a/core/lib/Drupal/Core/Config/Entity/ConfigEntityStorage.php
+++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityStorage.php
@@ -3,6 +3,7 @@
namespace Drupal\Core\Config\Entity;
use Drupal\Core\Cache\CacheableMetadata;
+use Drupal\Core\Cache\MemoryCache\MemoryCacheInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\ConfigImporterException;
use Drupal\Core\Entity\EntityInterface;
@@ -104,9 +105,11 @@ class ConfigEntityStorage extends EntityStorageBase implements ConfigEntityStora
* The UUID service.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
+ * @param \Drupal\Core\Cache\MemoryCache\MemoryCacheInterface|null $memory_cache
+ * The memory cache backend.
*/
- public function __construct(EntityTypeInterface $entity_type, ConfigFactoryInterface $config_factory, UuidInterface $uuid_service, LanguageManagerInterface $language_manager) {
- parent::__construct($entity_type);
+ public function __construct(EntityTypeInterface $entity_type, ConfigFactoryInterface $config_factory, UuidInterface $uuid_service, LanguageManagerInterface $language_manager, MemoryCacheInterface $memory_cache = NULL) {
+ parent::__construct($entity_type, $memory_cache);
$this->configFactory = $config_factory;
$this->uuidService = $uuid_service;
@@ -121,7 +124,8 @@ class ConfigEntityStorage extends EntityStorageBase implements ConfigEntityStora
$entity_type,
$container->get('config.factory'),
$container->get('uuid'),
- $container->get('language_manager')
+ $container->get('language_manager'),
+ $container->get('entity.memory_cache')
);
}
@@ -324,43 +328,10 @@ class ConfigEntityStorage extends EntityStorageBase implements ConfigEntityStora
}
/**
- * Gets entities from the static cache.
- *
- * @param array $ids
- * If not empty, return entities that match these IDs.
- *
- * @return \Drupal\Core\Entity\EntityInterface[]
- * Array of entities from the entity cache.
- */
- protected function getFromStaticCache(array $ids) {
- $entities = [];
- // Load any available entities from the internal cache.
- if ($this->entityType->isStaticallyCacheable() && !empty($this->entities)) {
- $config_overrides_key = $this->overrideFree ? '' : implode(':', $this->configFactory->getCacheKeys());
- foreach ($ids as $id) {
- if (!empty($this->entities[$id])) {
- if (isset($this->entities[$id][$config_overrides_key])) {
- $entities[$id] = $this->entities[$id][$config_overrides_key];
- }
- }
- }
- }
- return $entities;
- }
-
- /**
- * Stores entities in the static entity cache.
- *
- * @param \Drupal\Core\Entity\EntityInterface[] $entities
- * Entities to store in the cache.
- */
- protected function setStaticCache(array $entities) {
- if ($this->entityType->isStaticallyCacheable()) {
- $config_overrides_key = $this->overrideFree ? '' : implode(':', $this->configFactory->getCacheKeys());
- foreach ($entities as $id => $entity) {
- $this->entities[$id][$config_overrides_key] = $entity;
- }
- }
+ * {@inheritdoc}
+ */
+ protected function buildCacheId($id) {
+ return parent::buildCacheId($id) . ':' . ($this->overrideFree ? '' : implode(':', $this->configFactory->getCacheKeys()));
}
/**
@@ -445,7 +416,7 @@ class ConfigEntityStorage extends EntityStorageBase implements ConfigEntityStora
* @param bool $is_syncing
* Is the configuration entity being created as part of a config sync.
*
- * @return ConfigEntityInterface
+ * @return \Drupal\Core\Config\ConfigEntityInterface
* The configuration entity.
*
* @see \Drupal\Core\Config\Entity\ConfigEntityStorageInterface::createFromStorageRecord()
@@ -479,9 +450,27 @@ class ConfigEntityStorage extends EntityStorageBase implements ConfigEntityStora
$data = $this->mapFromStorageRecords([$values]);
$updated_entity = current($data);
- foreach (array_keys($values) as $property) {
- $value = $updated_entity->get($property);
- $entity->set($property, $value);
+ /** @var \Drupal\Core\Config\Entity\ConfigEntityTypeInterface $entity_type */
+ $entity_type = $this->getEntityType();
+ $id_key = $entity_type->getKey('id');
+ $properties = $entity_type->getPropertiesToExport($updated_entity->get($id_key));
+
+ if (empty($properties)) {
+ // Fallback to using the provided values. If the properties cannot be
+ // determined for the config entity type annotation or configuration
+ // schema.
+ $properties = array_keys($values);
+ }
+ foreach ($properties as $property) {
+ if ($property === $this->uuidKey) {
+ // During an update the UUID field should not be copied. Under regular
+ // circumstances the values will be equal. If configuration is written
+ // twice during configuration install the updated entity will not have a
+ // UUID.
+ // @see \Drupal\Core\Config\ConfigInstaller::createConfiguration()
+ continue;
+ }
+ $entity->set($property, $updated_entity->get($property));
}
return $entity;
diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigEntityType.php b/core/lib/Drupal/Core/Config/Entity/ConfigEntityType.php
index 11c9ff1..3f142df 100644
--- a/core/lib/Drupal/Core/Config/Entity/ConfigEntityType.php
+++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityType.php
@@ -142,30 +142,40 @@ class ConfigEntityType extends EntityType implements ConfigEntityTypeInterface {
/**
* {@inheritdoc}
*/
- public function getPropertiesToExport() {
+ public function getPropertiesToExport($id = NULL) {
+ if (!empty($this->mergedConfigExport)) {
+ return $this->mergedConfigExport;
+ }
if (!empty($this->config_export)) {
- if (empty($this->mergedConfigExport)) {
- // Always add default properties to be exported.
- $this->mergedConfigExport = [
- 'uuid' => 'uuid',
- 'langcode' => 'langcode',
- 'status' => 'status',
- 'dependencies' => 'dependencies',
- 'third_party_settings' => 'third_party_settings',
- '_core' => '_core',
- ];
- foreach ($this->config_export as $property => $name) {
- if (is_numeric($property)) {
- $this->mergedConfigExport[$name] = $name;
- }
- else {
- $this->mergedConfigExport[$property] = $name;
- }
+ // Always add default properties to be exported.
+ $this->mergedConfigExport = [
+ 'uuid' => 'uuid',
+ 'langcode' => 'langcode',
+ 'status' => 'status',
+ 'dependencies' => 'dependencies',
+ 'third_party_settings' => 'third_party_settings',
+ '_core' => '_core',
+ ];
+ foreach ($this->config_export as $property => $name) {
+ if (is_numeric($property)) {
+ $this->mergedConfigExport[$name] = $name;
+ }
+ else {
+ $this->mergedConfigExport[$property] = $name;
}
}
- return $this->mergedConfigExport;
}
- return NULL;
+ else {
+ // @todo https://www.drupal.org/project/drupal/issues/2949021 Deprecate
+ // fallback to schema.
+ $config_name = $this->getConfigPrefix() . '.' . $id;
+ $definition = \Drupal::service('config.typed')->getDefinition($config_name);
+ if (!isset($definition['mapping'])) {
+ return NULL;
+ }
+ $this->mergedConfigExport = array_combine(array_keys($definition['mapping']), array_keys($definition['mapping']));
+ }
+ return $this->mergedConfigExport;
}
/**
diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigEntityTypeInterface.php b/core/lib/Drupal/Core/Config/Entity/ConfigEntityTypeInterface.php
index 589a63d..de4b1e1 100644
--- a/core/lib/Drupal/Core/Config/Entity/ConfigEntityTypeInterface.php
+++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityTypeInterface.php
@@ -65,11 +65,18 @@ interface ConfigEntityTypeInterface extends EntityTypeInterface {
/**
* Gets the config entity properties to export if declared on the annotation.
*
+ * Falls back to determining the properties using configuration schema, if the
+ * config entity properties are not declared.
+ *
+ * @param string $id
+ * The ID of the configuration entity. Used when checking schema instead of
+ * the annotation.
+ *
* @return array|null
* The properties to export or NULL if they can not be determine from the
- * config entity type annotation.
+ * config entity type annotation or the schema.
*/
- public function getPropertiesToExport();
+ public function getPropertiesToExport($id = NULL);
/**
* Gets the keys that are available for fast lookup.
diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigEntityUpdater.php b/core/lib/Drupal/Core/Config/Entity/ConfigEntityUpdater.php
new file mode 100644
index 0000000..37e5fb1
--- /dev/null
+++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityUpdater.php
@@ -0,0 +1,119 @@
+<?php
+
+namespace Drupal\Core\Config\Entity;
+
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * A utility class to make updating configuration entities simple.
+ *
+ * Use this in a post update function like so:
+ * @code
+ * // Update the dependencies of all Vocabulary configuration entities.
+ * \Drupal::classResolver(ConfigEntityUpdater::class)->update($sandbox, 'taxonomy_vocabulary');
+ * @endcode
+ *
+ * The number of entities processed in each batch is determined by the
+ * 'entity_update_batch_size' setting.
+ *
+ * @see default.settings.php
+ */
+class ConfigEntityUpdater implements ContainerInjectionInterface {
+
+ /**
+ * The entity type manager.
+ *
+ * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+ */
+ protected $entityTypeManager;
+
+ /**
+ * The number of entities to process in each batch.
+ * @var int
+ */
+ protected $batchSize;
+
+ /**
+ * ConfigEntityUpdater constructor.
+ *
+ * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+ * The entity type manager.
+ * @param int $batch_size
+ * The number of entities to process in each batch.
+ */
+ public function __construct(EntityTypeManagerInterface $entity_type_manager, $batch_size) {
+ $this->entityTypeManager = $entity_type_manager;
+ $this->batchSize = $batch_size;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static(
+ $container->get('entity_type.manager'),
+ $container->get('settings')->get('entity_update_batch_size', 50)
+ );
+ }
+
+ /**
+ * Updates configuration entities as part of a Drupal update.
+ *
+ * @param array $sandbox
+ * Stores information for batch updates.
+ * @param string $entity_type_id
+ * The configuration entity type ID. For example, 'view' or 'vocabulary'.
+ * @param callable $callback
+ * (optional) A callback to determine if a configuration entity should be
+ * saved. The callback will be passed each entity of the provided type that
+ * exists. The callback should not save an entity itself. Return TRUE to
+ * save an entity. The callback can make changes to an entity. Note that all
+ * changes should comply with schema as an entity's data will not be
+ * validated against schema on save to avoid unexpected errors. If a
+ * callback is not provided, the default behaviour is to update the
+ * dependencies if required.
+ *
+ * @see hook_post_update_NAME()
+ *
+ * @api
+ *
+ * @throws \InvalidArgumentException
+ * Thrown when the provided entity type ID is not a configuration entity
+ * type.
+ */
+ public function update(array &$sandbox, $entity_type_id, callable $callback = NULL) {
+ $storage = $this->entityTypeManager->getStorage($entity_type_id);
+ $sandbox_key = 'config_entity_updater:' . $entity_type_id;
+ if (!isset($sandbox[$sandbox_key])) {
+ $entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
+ if (!($entity_type instanceof ConfigEntityTypeInterface)) {
+ throw new \InvalidArgumentException("The provided entity type ID '$entity_type_id' is not a configuration entity type");
+ }
+ $sandbox[$sandbox_key]['entities'] = $storage->getQuery()->accessCheck(FALSE)->execute();
+ $sandbox[$sandbox_key]['count'] = count($sandbox[$sandbox_key]['entities']);
+ }
+
+ // The default behaviour is to fix dependencies.
+ if ($callback === NULL) {
+ $callback = function ($entity) {
+ /** @var \Drupal\Core\Config\Entity\ConfigEntityInterface $entity */
+ $original_dependencies = $entity->getDependencies();
+ return $original_dependencies !== $entity->calculateDependencies()->getDependencies();
+ };
+ }
+
+ /** @var \Drupal\Core\Config\Entity\ConfigEntityInterface $entity */
+ $entities = $storage->loadMultiple(array_splice($sandbox[$sandbox_key]['entities'], 0, $this->batchSize));
+ foreach ($entities as $entity) {
+ if (call_user_func($callback, $entity)) {
+ $entity->trustData();
+ $entity->save();
+ }
+ }
+
+ $sandbox['#finished'] = empty($sandbox[$sandbox_key]['entities']) ? 1 : ($sandbox[$sandbox_key]['count'] - count($sandbox[$sandbox_key]['entities'])) / $sandbox[$sandbox_key]['count'];
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Config/Entity/DraggableListBuilder.php b/core/lib/Drupal/Core/Config/Entity/DraggableListBuilder.php
index f6bf463..d70aee8 100644
--- a/core/lib/Drupal/Core/Config/Entity/DraggableListBuilder.php
+++ b/core/lib/Drupal/Core/Config/Entity/DraggableListBuilder.php
@@ -107,7 +107,7 @@ abstract class DraggableListBuilder extends ConfigEntityListBuilder implements F
$form[$this->entitiesKey] = [
'#type' => 'table',
'#header' => $this->buildHeader(),
- '#empty' => t('There is no @label yet.', ['@label' => $this->entityType->getLabel()]),
+ '#empty' => t('There are no @label yet.', ['@label' => $this->entityType->getPluralLabel()]),
'#tabledrag' => [
[
'action' => 'order',
diff --git a/core/lib/Drupal/Core/Config/Entity/Query/Condition.php b/core/lib/Drupal/Core/Config/Entity/Query/Condition.php
index bd2facd..3410a6e 100644
--- a/core/lib/Drupal/Core/Config/Entity/Query/Condition.php
+++ b/core/lib/Drupal/Core/Config/Entity/Query/Condition.php
@@ -2,7 +2,6 @@
namespace Drupal\Core\Config\Entity\Query;
-use Drupal\Component\Utility\Unicode;
use Drupal\Core\Entity\Query\ConditionBase;
use Drupal\Core\Entity\Query\ConditionInterface;
use Drupal\Core\Entity\Query\QueryException;
@@ -32,10 +31,10 @@ class Condition extends ConditionBase {
// Lowercase condition value(s) for case-insensitive matches.
if (is_array($condition['value'])) {
- $condition['value'] = array_map('Drupal\Component\Utility\Unicode::strtolower', $condition['value']);
+ $condition['value'] = array_map('mb_strtolower', $condition['value']);
}
elseif (!is_bool($condition['value'])) {
- $condition['value'] = Unicode::strtolower($condition['value']);
+ $condition['value'] = mb_strtolower($condition['value']);
}
$single_conditions[] = $condition;
@@ -164,7 +163,7 @@ class Condition extends ConditionBase {
if (isset($value)) {
// We always want a case-insensitive match.
if (!is_bool($value)) {
- $value = Unicode::strtolower($value);
+ $value = mb_strtolower($value);
}
switch ($condition['operator']) {
diff --git a/core/lib/Drupal/Core/Config/Entity/Query/QueryFactory.php b/core/lib/Drupal/Core/Config/Entity/Query/QueryFactory.php
index f87e618..d784cdb 100644
--- a/core/lib/Drupal/Core/Config/Entity/Query/QueryFactory.php
+++ b/core/lib/Drupal/Core/Config/Entity/Query/QueryFactory.php
@@ -28,7 +28,7 @@ class QueryFactory implements QueryFactoryInterface, EventSubscriberInterface {
/**
* The config factory used by the config entity query.
*
- * @var \Drupal\Core\Config\ConfigFactoryInterface;
+ * @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
@@ -219,7 +219,7 @@ class QueryFactory implements QueryFactoryInterface, EventSubscriberInterface {
/**
* Updates configuration entity in the key store.
*
- * @param ConfigCrudEvent $event
+ * @param \Drupal\Core\Config\ConfigCrudEvent $event
* The configuration event.
*/
public function onConfigSave(ConfigCrudEvent $event) {
diff --git a/core/lib/Drupal/Core/Config/Entity/ThirdPartySettingsInterface.php b/core/lib/Drupal/Core/Config/Entity/ThirdPartySettingsInterface.php
index 7b2ee06..990a5b4 100644
--- a/core/lib/Drupal/Core/Config/Entity/ThirdPartySettingsInterface.php
+++ b/core/lib/Drupal/Core/Config/Entity/ThirdPartySettingsInterface.php
@@ -41,7 +41,6 @@ interface ThirdPartySettingsInterface {
*/
public function getThirdPartySetting($module, $key, $default = NULL);
-
/**
* Gets all third-party settings of a given module.
*
diff --git a/core/lib/Drupal/Core/Config/FileStorage.php b/core/lib/Drupal/Core/Config/FileStorage.php
index 84d23d3..60c5fa1 100644
--- a/core/lib/Drupal/Core/Config/FileStorage.php
+++ b/core/lib/Drupal/Core/Config/FileStorage.php
@@ -46,7 +46,7 @@ class FileStorage implements StorageInterface {
$this->collection = $collection;
// Use a NULL File Cache backend by default. This will ensure only the
- // internal statc caching of FileCache is used and thus avoids blowing up
+ // internal static caching of FileCache is used and thus avoids blowing up
// the APCu cache.
$this->fileCache = FileCacheFactory::get('config', ['cache_backend_class' => NULL]);
}
@@ -279,6 +279,9 @@ class FileStorage implements StorageInterface {
* {@inheritdoc}
*/
public function getAllCollectionNames() {
+ if (!is_dir($this->directory)) {
+ return [];
+ }
$collections = $this->getAllCollectionNamesHelper($this->directory);
sort($collections);
return $collections;
@@ -305,7 +308,8 @@ class FileStorage implements StorageInterface {
* @param string $directory
* The directory to check for sub directories. This allows this
* function to be used recursively to discover all the collections in the
- * storage.
+ * storage. It is the responsibility of the caller to ensure the directory
+ * exists.
*
* @return array
* A list of collection names contained within the provided directory.
diff --git a/core/lib/Drupal/Core/Config/Importer/ConfigImporterBatch.php b/core/lib/Drupal/Core/Config/Importer/ConfigImporterBatch.php
new file mode 100644
index 0000000..8aee289
--- /dev/null
+++ b/core/lib/Drupal/Core/Config/Importer/ConfigImporterBatch.php
@@ -0,0 +1,77 @@
+<?php
+
+namespace Drupal\Core\Config\Importer;
+
+use Drupal\Core\Config\ConfigImporter;
+
+/**
+ * Methods for running the ConfigImporter in a batch.
+ *
+ * @see \Drupal\Core\Config\ConfigImporter
+ */
+class ConfigImporterBatch {
+
+ /**
+ * Processes the config import batch and persists the importer.
+ *
+ * @param \Drupal\Core\Config\ConfigImporter $config_importer
+ * The batch config importer object to persist.
+ * @param string $sync_step
+ * The synchronization step to do.
+ * @param array $context
+ * The batch context.
+ */
+ public static function process(ConfigImporter $config_importer, $sync_step, &$context) {
+ if (!isset($context['sandbox']['config_importer'])) {
+ $context['sandbox']['config_importer'] = $config_importer;
+ }
+
+ $config_importer = $context['sandbox']['config_importer'];
+ $config_importer->doSyncStep($sync_step, $context);
+ if ($errors = $config_importer->getErrors()) {
+ if (!isset($context['results']['errors'])) {
+ $context['results']['errors'] = [];
+ }
+ $context['results']['errors'] = array_merge($errors, $context['results']['errors']);
+ }
+ }
+
+ /**
+ * Finish batch.
+ *
+ * This function is a static function to avoid serializing the ConfigSync
+ * object unnecessarily.
+ *
+ * @param bool $success
+ * Indicate that the batch API tasks were all completed successfully.
+ * @param array $results
+ * An array of all the results that were updated in update_do_one().
+ * @param array $operations
+ * A list of the operations that had not been completed by the batch API.
+ */
+ public static function finish($success, $results, $operations) {
+ $messenger = \Drupal::messenger();
+ if ($success) {
+ if (!empty($results['errors'])) {
+ $logger = \Drupal::logger('config_sync');
+ foreach ($results['errors'] as $error) {
+ $messenger->addError($error);
+ $logger->error($error);
+ }
+ $messenger->addWarning(t('The configuration was imported with errors.'));
+ }
+ elseif (!drupal_installation_attempted()) {
+ // Display a success message when not installing Drupal.
+ $messenger->addStatus(t('The configuration was imported successfully.'));
+ }
+ }
+ else {
+ // An error occurred.
+ // $operations contains the operations that remained unprocessed.
+ $error_operation = reset($operations);
+ $message = t('An error occurred while processing %error_operation with arguments: @arguments', ['%error_operation' => $error_operation[0], '@arguments' => print_r($error_operation[1], TRUE)]);
+ $messenger->addError($message);
+ }
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Config/PreExistingConfigException.php b/core/lib/Drupal/Core/Config/PreExistingConfigException.php
index 57f54b2..a71604e 100644
--- a/core/lib/Drupal/Core/Config/PreExistingConfigException.php
+++ b/core/lib/Drupal/Core/Config/PreExistingConfigException.php
@@ -2,7 +2,7 @@
namespace Drupal\Core\Config;
-use Drupal\Component\Utility\SafeMarkup;
+use Drupal\Component\Render\FormattableMarkup;
/**
* An exception thrown if configuration with the same name already exists.
@@ -56,10 +56,10 @@ class PreExistingConfigException extends ConfigException {
* @return \Drupal\Core\Config\PreExistingConfigException
*/
public static function create($extension, array $config_objects) {
- $message = SafeMarkup::format('Configuration objects (@config_names) provided by @extension already exist in active configuration',
+ $message = new FormattableMarkup('Configuration objects (@config_names) provided by @extension already exist in active configuration',
[
'@config_names' => implode(', ', static::flattenConfigObjects($config_objects)),
- '@extension' => $extension
+ '@extension' => $extension,
]
);
$e = new static($message);
diff --git a/core/lib/Drupal/Core/Config/UnmetDependenciesException.php b/core/lib/Drupal/Core/Config/UnmetDependenciesException.php
index 6c37c76..573ec54 100644
--- a/core/lib/Drupal/Core/Config/UnmetDependenciesException.php
+++ b/core/lib/Drupal/Core/Config/UnmetDependenciesException.php
@@ -92,7 +92,7 @@ class UnmetDependenciesException extends ConfigException {
$message = new FormattableMarkup('Configuration objects provided by %extension have unmet dependencies: %config_names',
[
'%config_names' => static::formatConfigObjectList($config_objects),
- '%extension' => $extension
+ '%extension' => $extension,
]
);
$e = new static($message);
diff --git a/core/lib/Drupal/Core/Controller/ArgumentResolver/Psr7RequestValueResolver.php b/core/lib/Drupal/Core/Controller/ArgumentResolver/Psr7RequestValueResolver.php
new file mode 100644
index 0000000..f5ce9f3
--- /dev/null
+++ b/core/lib/Drupal/Core/Controller/ArgumentResolver/Psr7RequestValueResolver.php
@@ -0,0 +1,47 @@
+<?php
+
+namespace Drupal\Core\Controller\ArgumentResolver;
+
+use Psr\Http\Message\ServerRequestInterface;
+use Symfony\Bridge\PsrHttpMessage\HttpMessageFactoryInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface;
+use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
+
+/**
+ * Yields a PSR7 request object based on the request object passed along.
+ */
+final class Psr7RequestValueResolver implements ArgumentValueResolverInterface {
+
+ /**
+ * The PSR-7 converter.
+ *
+ * @var \Symfony\Bridge\PsrHttpMessage\HttpMessageFactoryInterface
+ */
+ protected $httpMessageFactory;
+
+ /**
+ * Constructs a new ControllerResolver.
+ *
+ * @param \Symfony\Bridge\PsrHttpMessage\HttpMessageFactoryInterface $http_message_factory
+ * The PSR-7 converter.
+ */
+ public function __construct(HttpMessageFactoryInterface $http_message_factory) {
+ $this->httpMessageFactory = $http_message_factory;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function supports(Request $request, ArgumentMetadata $argument) {
+ return $argument->getType() == ServerRequestInterface::class;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function resolve(Request $request, ArgumentMetadata $argument) {
+ yield $this->httpMessageFactory->createRequest($request);
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Controller/ArgumentResolver/RawParameterValueResolver.php b/core/lib/Drupal/Core/Controller/ArgumentResolver/RawParameterValueResolver.php
new file mode 100644
index 0000000..7e2a35a
--- /dev/null
+++ b/core/lib/Drupal/Core/Controller/ArgumentResolver/RawParameterValueResolver.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace Drupal\Core\Controller\ArgumentResolver;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface;
+use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
+
+/**
+ * Yields an argument's value from the request's _raw_variables attribute.
+ */
+final class RawParameterValueResolver implements ArgumentValueResolverInterface {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function supports(Request $request, ArgumentMetadata $argument) {
+ return !$argument->isVariadic() && $request->attributes->has('_raw_variables') && array_key_exists($argument->getName(), $request->attributes->get('_raw_variables'));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function resolve(Request $request, ArgumentMetadata $argument) {
+ yield $request->attributes->get('_raw_variables')[$argument->getName()];
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Controller/ArgumentResolver/RouteMatchValueResolver.php b/core/lib/Drupal/Core/Controller/ArgumentResolver/RouteMatchValueResolver.php
new file mode 100644
index 0000000..d5846fe
--- /dev/null
+++ b/core/lib/Drupal/Core/Controller/ArgumentResolver/RouteMatchValueResolver.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace Drupal\Core\Controller\ArgumentResolver;
+
+use Drupal\Core\Routing\RouteMatch;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface;
+use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
+
+/**
+ * Yields a RouteMatch object based on the request object passed along.
+ */
+final class RouteMatchValueResolver implements ArgumentValueResolverInterface {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function supports(Request $request, ArgumentMetadata $argument) {
+ return $argument->getType() == RouteMatchInterface::class || is_subclass_of($argument->getType(), RouteMatchInterface::class);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function resolve(Request $request, ArgumentMetadata $argument) {
+ yield RouteMatch::createFromRequest($request);
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Controller/ControllerBase.php b/core/lib/Drupal/Core/Controller/ControllerBase.php
index c0d0cdd..68c2655 100644
--- a/core/lib/Drupal/Core/Controller/ControllerBase.php
+++ b/core/lib/Drupal/Core/Controller/ControllerBase.php
@@ -9,6 +9,7 @@ use Drupal\Core\Routing\RedirectDestinationTrait;
use Drupal\Core\Routing\UrlGeneratorTrait;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;
+use Drupal\Core\Messenger\MessengerTrait;
/**
* Utility base class for thin controllers.
@@ -35,6 +36,7 @@ abstract class ControllerBase implements ContainerInjectionInterface {
use LinkGeneratorTrait;
use LoggerChannelTrait;
+ use MessengerTrait;
use RedirectDestinationTrait;
use StringTranslationTrait;
use UrlGeneratorTrait;
@@ -91,7 +93,7 @@ abstract class ControllerBase implements ContainerInjectionInterface {
/**
* The state service.
*
- * @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface
+ * @var \Drupal\Core\State\StateInterface
*/
protected $stateService;
@@ -220,7 +222,7 @@ abstract class ControllerBase implements ContainerInjectionInterface {
* needs to be the same across development, production, etc. environments
* (for example, the system maintenance message) should use config() instead.
*
- * @return \Drupal\Core\KeyValueStore\KeyValueStoreInterface
+ * @return \Drupal\Core\State\StateInterface
*/
protected function state() {
if (!$this->stateService) {
diff --git a/core/lib/Drupal/Core/Controller/ControllerResolver.php b/core/lib/Drupal/Core/Controller/ControllerResolver.php
index c16a087..35d433a 100644
--- a/core/lib/Drupal/Core/Controller/ControllerResolver.php
+++ b/core/lib/Drupal/Core/Controller/ControllerResolver.php
@@ -80,7 +80,6 @@ class ControllerResolver extends BaseControllerResolver implements ControllerRes
return $callable;
}
-
/**
* {@inheritdoc}
*/
@@ -129,6 +128,10 @@ class ControllerResolver extends BaseControllerResolver implements ControllerRes
* {@inheritdoc}
*/
protected function doGetArguments(Request $request, $controller, array $parameters) {
+ // Note this duplicates the deprecation message of
+ // Symfony\Component\HttpKernel\Controller\ControllerResolver::getArguments()
+ // to ensure it is removed in Drupal 9.
+ @trigger_error(sprintf('%s is deprecated as of 8.6.0 and will be removed in 9.0. Inject the "http_kernel.controller.argument_resolver" service instead.', __METHOD__, ArgumentResolverInterface::class), E_USER_DEPRECATED);
$attributes = $request->attributes->all();
$raw_parameters = $request->attributes->has('_raw_variables') ? $request->attributes->get('_raw_variables') : [];
$arguments = [];
diff --git a/core/lib/Drupal/Core/Controller/FormController.php b/core/lib/Drupal/Core/Controller/FormController.php
index 7af86df..acbcaf6 100644
--- a/core/lib/Drupal/Core/Controller/FormController.php
+++ b/core/lib/Drupal/Core/Controller/FormController.php
@@ -7,6 +7,7 @@ use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Form\FormState;
use Drupal\Core\Routing\RouteMatchInterface;
use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface;
/**
* Common base class for form interstitial controllers.
@@ -17,9 +18,23 @@ abstract class FormController {
use DependencySerializationTrait;
/**
+ * The argument resolver.
+ *
+ * @var \Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface
+ */
+ protected $argumentResolver;
+
+ /**
* The controller resolver.
*
* @var \Drupal\Core\Controller\ControllerResolverInterface
+ *
+ * @deprecated
+ * Deprecated property that is only assigned when the 'controller_resolver'
+ * service is used as the first parameter to FormController::__construct().
+ *
+ * @see https://www.drupal.org/node/2959408
+ * @see \Drupal\Core\Controller\FormController::__construct()
*/
protected $controllerResolver;
@@ -33,13 +48,17 @@ abstract class FormController {
/**
* Constructs a new \Drupal\Core\Controller\FormController object.
*
- * @param \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver
- * The controller resolver.
+ * @param \Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface $argument_resolver
+ * The argument resolver.
* @param \Drupal\Core\Form\FormBuilderInterface $form_builder
* The form builder.
*/
- public function __construct(ControllerResolverInterface $controller_resolver, FormBuilderInterface $form_builder) {
- $this->controllerResolver = $controller_resolver;
+ public function __construct(ArgumentResolverInterface $argument_resolver, FormBuilderInterface $form_builder) {
+ $this->argumentResolver = $argument_resolver;
+ if ($argument_resolver instanceof ControllerResolverInterface) {
+ @trigger_error("Using the 'controller_resolver' service as the first argument is deprecated, use the 'http_kernel.controller.argument_resolver' instead. If your subclass requires the 'controller_resolver' service add it as an additional argument. See https://www.drupal.org/node/2959408.", E_USER_DEPRECATED);
+ $this->controllerResolver = $argument_resolver;
+ }
$this->formBuilder = $form_builder;
}
@@ -63,7 +82,7 @@ abstract class FormController {
$form_state = new FormState();
$request->attributes->set('form', []);
$request->attributes->set('form_state', $form_state);
- $args = $this->controllerResolver->getArguments($request, [$form_object, 'buildForm']);
+ $args = $this->argumentResolver->getArguments($request, [$form_object, 'buildForm']);
$request->attributes->remove('form');
$request->attributes->remove('form_state');
diff --git a/core/lib/Drupal/Core/Controller/HtmlFormController.php b/core/lib/Drupal/Core/Controller/HtmlFormController.php
index fe63038..6dd27ee 100644
--- a/core/lib/Drupal/Core/Controller/HtmlFormController.php
+++ b/core/lib/Drupal/Core/Controller/HtmlFormController.php
@@ -5,6 +5,7 @@ namespace Drupal\Core\Controller;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\DependencyInjection\ClassResolverInterface;
+use Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface;
/**
* Wrapping controller for forms that serve as the main page body.
@@ -14,22 +15,22 @@ class HtmlFormController extends FormController {
/**
* The class resolver.
*
- * @var \Drupal\Core\DependencyInjection\ClassResolverInterface;
+ * @var \Drupal\Core\DependencyInjection\ClassResolverInterface
*/
protected $classResolver;
/**
* Constructs a new \Drupal\Core\Routing\Enhancer\FormEnhancer object.
*
- * @param \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver
- * The controller resolver.
+ * @param \Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface $argument_resolver
+ * The argument resolver.
* @param \Drupal\Core\Form\FormBuilderInterface $form_builder
* The form builder.
* @param \Drupal\Core\DependencyInjection\ClassResolverInterface $class_resolver
* The class resolver.
*/
- public function __construct(ControllerResolverInterface $controller_resolver, FormBuilderInterface $form_builder, ClassResolverInterface $class_resolver) {
- parent::__construct($controller_resolver, $form_builder);
+ public function __construct(ArgumentResolverInterface $argument_resolver, FormBuilderInterface $form_builder, ClassResolverInterface $class_resolver) {
+ parent::__construct($argument_resolver, $form_builder);
$this->classResolver = $class_resolver;
}
diff --git a/core/lib/Drupal/Core/Controller/TitleResolver.php b/core/lib/Drupal/Core/Controller/TitleResolver.php
index 954835d..85fce77 100644
--- a/core/lib/Drupal/Core/Controller/TitleResolver.php
+++ b/core/lib/Drupal/Core/Controller/TitleResolver.php
@@ -5,6 +5,7 @@ namespace Drupal\Core\Controller;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface;
use Symfony\Component\Routing\Route;
/**
@@ -21,16 +22,26 @@ class TitleResolver implements TitleResolverInterface {
protected $controllerResolver;
/**
+ * The argument resolver.
+ *
+ * @var \Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface
+ */
+ protected $argumentResolver;
+
+ /**
* Constructs a TitleResolver instance.
*
* @param \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver
* The controller resolver.
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* The translation manager.
+ * @param \Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface $argument_resolver
+ * The argument resolver.
*/
- public function __construct(ControllerResolverInterface $controller_resolver, TranslationInterface $string_translation) {
+ public function __construct(ControllerResolverInterface $controller_resolver, TranslationInterface $string_translation, ArgumentResolverInterface $argument_resolver) {
$this->controllerResolver = $controller_resolver;
$this->stringTranslation = $string_translation;
+ $this->argumentResolver = $argument_resolver;
}
/**
@@ -43,7 +54,7 @@ class TitleResolver implements TitleResolverInterface {
// trying to use empty values.
if ($callback = $route->getDefault('_title_callback')) {
$callable = $this->controllerResolver->getControllerFromDefinition($callback);
- $arguments = $this->controllerResolver->getArguments($request, $callable);
+ $arguments = $this->argumentResolver->getArguments($request, $callable);
$route_title = call_user_func_array($callable, $arguments);
}
elseif ($title = $route->getDefault('_title')) {
diff --git a/core/lib/Drupal/Core/Database/Connection.php b/core/lib/Drupal/Core/Database/Connection.php
index 100ef68..f451e50 100644
--- a/core/lib/Drupal/Core/Database/Connection.php
+++ b/core/lib/Drupal/Core/Database/Connection.php
@@ -808,12 +808,15 @@ abstract class Connection {
* @param string $table
* The table to use for the insert statement.
* @param array $options
- * (optional) An array of options on the query.
+ * (optional) An associative array of options to control how the query is
+ * run. The given options will be merged with
+ * \Drupal\Core\Database\Connection::defaultOptions().
*
* @return \Drupal\Core\Database\Query\Insert
* A new Insert query object.
*
* @see \Drupal\Core\Database\Query\Insert
+ * @see \Drupal\Core\Database\Connection::defaultOptions()
*/
public function insert($table, array $options = []) {
$class = $this->getDriverClass('Insert');
@@ -862,12 +865,15 @@ abstract class Connection {
* @param string $table
* The table to use for the update statement.
* @param array $options
- * (optional) An array of options on the query.
+ * (optional) An associative array of options to control how the query is
+ * run. The given options will be merged with
+ * \Drupal\Core\Database\Connection::defaultOptions().
*
* @return \Drupal\Core\Database\Query\Update
* A new Update query object.
*
* @see \Drupal\Core\Database\Query\Update
+ * @see \Drupal\Core\Database\Connection::defaultOptions()
*/
public function update($table, array $options = []) {
$class = $this->getDriverClass('Update');
@@ -880,12 +886,15 @@ abstract class Connection {
* @param string $table
* The table to use for the delete statement.
* @param array $options
- * (optional) An array of options on the query.
+ * (optional) An associative array of options to control how the query is
+ * run. The given options will be merged with
+ * \Drupal\Core\Database\Connection::defaultOptions().
*
* @return \Drupal\Core\Database\Query\Delete
* A new Delete query object.
*
* @see \Drupal\Core\Database\Query\Delete
+ * @see \Drupal\Core\Database\Connection::defaultOptions()
*/
public function delete($table, array $options = []) {
$class = $this->getDriverClass('Delete');
@@ -1472,4 +1481,116 @@ abstract class Connection {
throw new \LogicException('The database connection is not serializable. This probably means you are serializing an object that has an indirect reference to the database connection. Adjust your code so that is not necessary. Alternatively, look at DependencySerializationTrait as a temporary solution.');
}
+ /**
+ * Creates an array of database connection options from a URL.
+ *
+ * @internal
+ * This method should not be called. Use
+ * \Drupal\Core\Database\Database::convertDbUrlToConnectionInfo() instead.
+ *
+ * @param string $url
+ * The URL.
+ * @param string $root
+ * The root directory of the Drupal installation. Some database drivers,
+ * like for example SQLite, need this information.
+ *
+ * @return array
+ * The connection options.
+ *
+ * @throws \InvalidArgumentException
+ * Exception thrown when the provided URL does not meet the minimum
+ * requirements.
+ *
+ * @see \Drupal\Core\Database\Database::convertDbUrlToConnectionInfo()
+ */
+ public static function createConnectionOptionsFromUrl($url, $root) {
+ $url_components = parse_url($url);
+ if (!isset($url_components['scheme'], $url_components['host'], $url_components['path'])) {
+ throw new \InvalidArgumentException('Minimum requirement: driver://host/database');
+ }
+
+ $url_components += [
+ 'user' => '',
+ 'pass' => '',
+ 'fragment' => '',
+ ];
+
+ // Remove leading slash from the URL path.
+ if ($url_components['path'][0] === '/') {
+ $url_components['path'] = substr($url_components['path'], 1);
+ }
+
+ // Use reflection to get the namespace of the class being called.
+ $reflector = new \ReflectionClass(get_called_class());
+
+ $database = [
+ 'driver' => $url_components['scheme'],
+ 'username' => $url_components['user'],
+ 'password' => $url_components['pass'],
+ 'host' => $url_components['host'],
+ 'database' => $url_components['path'],
+ 'namespace' => $reflector->getNamespaceName(),
+ ];
+
+ if (isset($url_components['port'])) {
+ $database['port'] = $url_components['port'];
+ }
+
+ if (!empty($url_components['fragment'])) {
+ $database['prefix']['default'] = $url_components['fragment'];
+ }
+
+ return $database;
+ }
+
+ /**
+ * Creates a URL from an array of database connection options.
+ *
+ * @internal
+ * This method should not be called. Use
+ * \Drupal\Core\Database\Database::getConnectionInfoAsUrl() instead.
+ *
+ * @param array $connection_options
+ * The array of connection options for a database connection.
+ *
+ * @return string
+ * The connection info as a URL.
+ *
+ * @throws \InvalidArgumentException
+ * Exception thrown when the provided array of connection options does not
+ * meet the minimum requirements.
+ *
+ * @see \Drupal\Core\Database\Database::getConnectionInfoAsUrl()
+ */
+ public static function createUrlFromConnectionOptions(array $connection_options) {
+ if (!isset($connection_options['driver'], $connection_options['database'])) {
+ throw new \InvalidArgumentException("As a minimum, the connection options array must contain at least the 'driver' and 'database' keys");
+ }
+
+ $user = '';
+ if (isset($connection_options['username'])) {
+ $user = $connection_options['username'];
+ if (isset($connection_options['password'])) {
+ $user .= ':' . $connection_options['password'];
+ }
+ $user .= '@';
+ }
+
+ $host = empty($connection_options['host']) ? 'localhost' : $connection_options['host'];
+
+ $db_url = $connection_options['driver'] . '://' . $user . $host;
+
+ if (isset($connection_options['port'])) {
+ $db_url .= ':' . $connection_options['port'];
+ }
+
+ $db_url .= '/' . $connection_options['database'];
+
+ if (isset($connection_options['prefix']['default']) && $connection_options['prefix']['default'] !== '') {
+ $db_url .= '#' . $connection_options['prefix']['default'];
+ }
+
+ return $db_url;
+ }
+
}
diff --git a/core/lib/Drupal/Core/Database/Database.php b/core/lib/Drupal/Core/Database/Database.php
index dd19018..f29b60e 100644
--- a/core/lib/Drupal/Core/Database/Database.php
+++ b/core/lib/Drupal/Core/Database/Database.php
@@ -365,13 +365,8 @@ abstract class Database {
throw new DriverNotSpecifiedException('Driver not specified for this database connection: ' . $key);
}
- if (!empty(self::$databaseInfo[$key][$target]['namespace'])) {
- $driver_class = self::$databaseInfo[$key][$target]['namespace'] . '\\Connection';
- }
- else {
- // Fallback for Drupal 7 settings.php.
- $driver_class = "Drupal\\Core\\Database\\Driver\\{$driver}\\Connection";
- }
+ $namespace = static::getDatabaseDriverNamespace(self::$databaseInfo[$key][$target]);
+ $driver_class = $namespace . '\\Connection';
$pdo_connection = $driver_class::open(self::$databaseInfo[$key][$target]);
$new_connection = new $driver_class($pdo_connection, self::$databaseInfo[$key][$target]);
@@ -455,36 +450,25 @@ abstract class Database {
* requirements.
*/
public static function convertDbUrlToConnectionInfo($url, $root) {
- $info = parse_url($url);
- if (!isset($info['scheme'], $info['host'], $info['path'])) {
- throw new \InvalidArgumentException('Minimum requirement: driver://host/database');
+ // Check that the URL is well formed, starting with 'scheme://', where
+ // 'scheme' is a database driver name.
+ if (preg_match('/^(.*):\/\//', $url, $matches) !== 1) {
+ throw new \InvalidArgumentException("Missing scheme in URL '$url'");
}
- $info += [
- 'user' => '',
- 'pass' => '',
- 'fragment' => '',
- ];
-
- // A SQLite database path with two leading slashes indicates a system path.
- // Otherwise the path is relative to the Drupal root.
- if ($info['path'][0] === '/') {
- $info['path'] = substr($info['path'], 1);
- }
- if ($info['scheme'] === 'sqlite' && $info['path'][0] !== '/') {
- $info['path'] = $root . '/' . $info['path'];
+ $driver = $matches[1];
+
+ // Discover if the URL has a valid driver scheme. Try with core drivers
+ // first.
+ $connection_class = "Drupal\\Core\\Database\\Driver\\{$driver}\\Connection";
+ if (!class_exists($connection_class)) {
+ // If the URL is not relative to a core driver, try with custom ones.
+ $connection_class = "Drupal\\Driver\\Database\\{$driver}\\Connection";
+ if (!class_exists($connection_class)) {
+ throw new \InvalidArgumentException("Can not convert '$url' to a database connection, class '$connection_class' does not exist");
+ }
}
- $database = [
- 'driver' => $info['scheme'],
- 'username' => $info['user'],
- 'password' => $info['pass'],
- 'host' => $info['host'],
- 'database' => $info['path'],
- ];
- if (isset($info['port'])) {
- $database['port'] = $info['port'];
- }
- return $database;
+ return $connection_class::createConnectionOptionsFromUrl($url, $root);
}
/**
@@ -495,32 +479,36 @@ abstract class Database {
*
* @return string
* The connection info as a URL.
+ *
+ * @throws \RuntimeException
+ * When the database connection is not defined.
*/
public static function getConnectionInfoAsUrl($key = 'default') {
$db_info = static::getConnectionInfo($key);
- if ($db_info['default']['driver'] == 'sqlite') {
- $db_url = 'sqlite://localhost/' . $db_info['default']['database'];
+ if (empty($db_info) || empty($db_info['default'])) {
+ throw new \RuntimeException("Database connection $key not defined or missing the 'default' settings");
}
- else {
- $user = '';
- if ($db_info['default']['username']) {
- $user = $db_info['default']['username'];
- if ($db_info['default']['password']) {
- $user .= ':' . $db_info['default']['password'];
- }
- $user .= '@';
- }
+ $connection_class = static::getDatabaseDriverNamespace($db_info['default']) . '\\Connection';
+ return $connection_class::createUrlFromConnectionOptions($db_info['default']);
+ }
- $db_url = $db_info['default']['driver'] . '://' . $user . $db_info['default']['host'];
- if (isset($db_info['default']['port'])) {
- $db_url .= ':' . $db_info['default']['port'];
- }
- $db_url .= '/' . $db_info['default']['database'];
- }
- if ($db_info['default']['prefix']['default']) {
- $db_url .= '#' . $db_info['default']['prefix']['default'];
+ /**
+ * Gets the PHP namespace of a database driver from the connection info.
+ *
+ * @param array $connection_info
+ * The database connection information, as defined in settings.php. The
+ * structure of this array depends on the database driver it is connecting
+ * to.
+ *
+ * @return string
+ * The PHP namespace of the driver's database.
+ */
+ protected static function getDatabaseDriverNamespace(array $connection_info) {
+ if (isset($connection_info['namespace'])) {
+ return $connection_info['namespace'];
}
- return $db_url;
+ // Fallback for Drupal 7 settings.php.
+ return 'Drupal\\Core\\Database\\Driver\\' . $connection_info['driver'];
}
}
diff --git a/core/lib/Drupal/Core/Database/Driver/mysql/Connection.php b/core/lib/Drupal/Core/Database/Driver/mysql/Connection.php
index 164f68a..626be4b 100644
--- a/core/lib/Drupal/Core/Database/Driver/mysql/Connection.php
+++ b/core/lib/Drupal/Core/Database/Driver/mysql/Connection.php
@@ -65,6 +65,277 @@ class Connection extends DatabaseConnection {
const MIN_MAX_ALLOWED_PACKET = 1024;
/**
+ * The list of MySQL reserved key words.
+ *
+ * @link https://dev.mysql.com/doc/refman/8.0/en/keywords.html
+ */
+ private $reservedKeyWords = [
+ 'accessible',
+ 'add',
+ 'admin',
+ 'all',
+ 'alter',
+ 'analyze',
+ 'and',
+ 'as',
+ 'asc',
+ 'asensitive',
+ 'before',
+ 'between',
+ 'bigint',
+ 'binary',
+ 'blob',
+ 'both',
+ 'by',
+ 'call',
+ 'cascade',
+ 'case',
+ 'change',
+ 'char',
+ 'character',
+ 'check',
+ 'collate',
+ 'column',
+ 'condition',
+ 'constraint',
+ 'continue',
+ 'convert',
+ 'create',
+ 'cross',
+ 'cube',
+ 'cume_dist',
+ 'current_date',
+ 'current_time',
+ 'current_timestamp',
+ 'current_user',
+ 'cursor',
+ 'database',
+ 'databases',
+ 'day_hour',
+ 'day_microsecond',
+ 'day_minute',
+ 'day_second',
+ 'dec',
+ 'decimal',
+ 'declare',
+ 'default',
+ 'delayed',
+ 'delete',
+ 'dense_rank',
+ 'desc',
+ 'describe',
+ 'deterministic',
+ 'distinct',
+ 'distinctrow',
+ 'div',
+ 'double',
+ 'drop',
+ 'dual',
+ 'each',
+ 'else',
+ 'elseif',
+ 'empty',
+ 'enclosed',
+ 'escaped',
+ 'except',
+ 'exists',
+ 'exit',
+ 'explain',
+ 'false',
+ 'fetch',
+ 'first_value',
+ 'float',
+ 'float4',
+ 'float8',
+ 'for',
+ 'force',
+ 'foreign',
+ 'from',
+ 'fulltext',
+ 'function',
+ 'generated',
+ 'get',
+ 'grant',
+ 'group',
+ 'grouping',
+ 'groups',
+ 'having',
+ 'high_priority',
+ 'hour_microsecond',
+ 'hour_minute',
+ 'hour_second',
+ 'if',
+ 'ignore',
+ 'in',
+ 'index',
+ 'infile',
+ 'inner',
+ 'inout',
+ 'insensitive',
+ 'insert',
+ 'int',
+ 'int1',
+ 'int2',
+ 'int3',
+ 'int4',
+ 'int8',
+ 'integer',
+ 'interval',
+ 'into',
+ 'io_after_gtids',
+ 'io_before_gtids',
+ 'is',
+ 'iterate',
+ 'join',
+ 'json_table',
+ 'key',
+ 'keys',
+ 'kill',
+ 'lag',
+ 'last_value',
+ 'lead',
+ 'leading',
+ 'leave',
+ 'left',
+ 'like',
+ 'limit',
+ 'linear',
+ 'lines',
+ 'load',
+ 'localtime',
+ 'localtimestamp',
+ 'lock',
+ 'long',
+ 'longblob',
+ 'longtext',
+ 'loop',
+ 'low_priority',
+ 'master_bind',
+ 'master_ssl_verify_server_cert',
+ 'match',
+ 'maxvalue',
+ 'mediumblob',
+ 'mediumint',
+ 'mediumtext',
+ 'middleint',
+ 'minute_microsecond',
+ 'minute_second',
+ 'mod',
+ 'modifies',
+ 'natural',
+ 'not',
+ 'no_write_to_binlog',
+ 'nth_value',
+ 'ntile',
+ 'null',
+ 'numeric',
+ 'of',
+ 'on',
+ 'optimize',
+ 'optimizer_costs',
+ 'option',
+ 'optionally',
+ 'or',
+ 'order',
+ 'out',
+ 'outer',
+ 'outfile',
+ 'over',
+ 'partition',
+ 'percent_rank',
+ 'persist',
+ 'persist_only',
+ 'precision',
+ 'primary',
+ 'procedure',
+ 'purge',
+ 'range',
+ 'rank',
+ 'read',
+ 'reads',
+ 'read_write',
+ 'real',
+ 'recursive',
+ 'references',
+ 'regexp',
+ 'release',
+ 'rename',
+ 'repeat',
+ 'replace',
+ 'require',
+ 'resignal',
+ 'restrict',
+ 'return',
+ 'revoke',
+ 'right',
+ 'rlike',
+ 'row',
+ 'rows',
+ 'row_number',
+ 'schema',
+ 'schemas',
+ 'second_microsecond',
+ 'select',
+ 'sensitive',
+ 'separator',
+ 'set',
+ 'show',
+ 'signal',
+ 'smallint',
+ 'spatial',
+ 'specific',
+ 'sql',
+ 'sqlexception',
+ 'sqlstate',
+ 'sqlwarning',
+ 'sql_big_result',
+ 'sql_calc_found_rows',
+ 'sql_small_result',
+ 'ssl',
+ 'starting',
+ 'stored',
+ 'straight_join',
+ 'system',
+ 'table',
+ 'terminated',
+ 'then',
+ 'tinyblob',
+ 'tinyint',
+ 'tinytext',
+ 'to',
+ 'trailing',
+ 'trigger',
+ 'true',
+ 'undo',
+ 'union',
+ 'unique',
+ 'unlock',
+ 'unsigned',
+ 'update',
+ 'usage',
+ 'use',
+ 'using',
+ 'utc_date',
+ 'utc_time',
+ 'utc_timestamp',
+ 'values',
+ 'varbinary',
+ 'varchar',
+ 'varcharacter',
+ 'varying',
+ 'virtual',
+ 'when',
+ 'where',
+ 'while',
+ 'window',
+ 'with',
+ 'write',
+ 'xor',
+ 'year_month',
+ 'zerofill',
+ ];
+
+ /**
* Constructs a Connection object.
*/
public function __construct(\PDO $connection, array $connection_options = []) {
@@ -160,7 +431,8 @@ class Connection extends DatabaseConnection {
// Force MySQL to use the UTF-8 character set. Also set the collation, if a
// certain one has been set; otherwise, MySQL defaults to
- // 'utf8mb4_general_ci' for utf8mb4.
+ // 'utf8mb4_general_ci' (MySQL 5) or 'utf8mb4_0900_ai_ci' (MySQL 8) for
+ // utf8mb4.
if (!empty($connection_options['collation'])) {
$pdo->exec('SET NAMES ' . $charset . ' COLLATE ' . $connection_options['collation']);
}
@@ -179,9 +451,18 @@ class Connection extends DatabaseConnection {
$connection_options += [
'init_commands' => [],
];
+
+ $sql_mode = 'ANSI,STRICT_TRANS_TABLES,STRICT_ALL_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,ONLY_FULL_GROUP_BY';
+ // NO_AUTO_CREATE_USER is removed in MySQL 8.0.11
+ // https://dev.mysql.com/doc/relnotes/mysql/8.0/en/news-8-0-11.html#mysqld-8-0-11-deprecation-removal
+ $version_server = $pdo->getAttribute(\PDO::ATTR_SERVER_VERSION);
+ if (version_compare($version_server, '8.0.11', '<')) {
+ $sql_mode .= ',NO_AUTO_CREATE_USER';
+ }
$connection_options['init_commands'] += [
- 'sql_mode' => "SET sql_mode = 'ANSI,STRICT_TRANS_TABLES,STRICT_ALL_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,ONLY_FULL_GROUP_BY'",
+ 'sql_mode' => "SET sql_mode = '$sql_mode'",
];
+
// Execute initial commands.
foreach ($connection_options['init_commands'] as $sql) {
$pdo->exec($sql);
@@ -193,6 +474,49 @@ class Connection extends DatabaseConnection {
/**
* {@inheritdoc}
*/
+ public function escapeField($field) {
+ $field = parent::escapeField($field);
+ return $this->quoteIdentifier($field);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function escapeAlias($field) {
+ // Quote fields so that MySQL reserved words like 'function' can be used
+ // as aliases.
+ $field = parent::escapeAlias($field);
+ return $this->quoteIdentifier($field);
+ }
+
+ /**
+ * Quotes an identifier if it matches a MySQL reserved keyword.
+ *
+ * @param string $identifier
+ * The field to check.
+ *
+ * @return string
+ * The identifier, quoted if it matches a MySQL reserved keyword.
+ */
+ private function quoteIdentifier($identifier) {
+ // Quote identifiers so that MySQL reserved words like 'function' can be
+ // used as column names. Sometimes the 'table.column_name' format is passed
+ // in. For example,
+ // \Drupal\Core\Entity\Sql\SqlContentEntityStorage::buildQuery() adds a
+ // condition on "base.uid" while loading user entities.
+ if (strpos($identifier, '.') !== FALSE) {
+ list($table, $identifier) = explode('.', $identifier, 2);
+ }
+ if (in_array(strtolower($identifier), $this->reservedKeyWords, TRUE)) {
+ // Quote the string for MySQL reserved keywords.
+ $identifier = '"' . $identifier . '"';
+ }
+ return isset($table) ? $table . '.' . $identifier : $identifier;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
public function serialize() {
// Cleanup the connection, much like __destruct() does it as well.
if ($this->needsCleanup) {
diff --git a/core/lib/Drupal/Core/Database/Driver/mysql/Insert.php b/core/lib/Drupal/Core/Database/Driver/mysql/Insert.php
index 8b7c602e..3d397c5 100644
--- a/core/lib/Drupal/Core/Database/Driver/mysql/Insert.php
+++ b/core/lib/Drupal/Core/Database/Driver/mysql/Insert.php
@@ -44,6 +44,10 @@ class Insert extends QueryInsert {
// Default fields are always placed first for consistency.
$insert_fields = array_merge($this->defaultFields, $this->insertFields);
+ $insert_fields = array_map(function ($field) {
+ return $this->connection->escapeField($field);
+ }, $insert_fields);
+
// If we're selecting from a SelectQuery, finish building the query and
// pass it back, as any remaining options are irrelevant.
if (!empty($this->fromQuery)) {
diff --git a/core/lib/Drupal/Core/Database/Driver/mysql/Install/Tasks.php b/core/lib/Drupal/Core/Database/Driver/mysql/Install/Tasks.php
index 0b1d7dd..17fc9a3 100644
--- a/core/lib/Drupal/Core/Database/Driver/mysql/Install/Tasks.php
+++ b/core/lib/Drupal/Core/Database/Driver/mysql/Install/Tasks.php
@@ -59,13 +59,13 @@ class Tasks extends InstallTasks {
protected function connect() {
try {
// This doesn't actually test the connection.
- db_set_active();
+ Database::setActiveConnection();
// Now actually do a check.
try {
Database::getConnection();
}
catch (\Exception $e) {
- // Detect utf8mb4 incompability.
+ // Detect utf8mb4 incompatibility.
if ($e->getCode() == Connection::UNSUPPORTED_CHARSET || ($e->getCode() == Connection::SQLSTATE_SYNTAX_ERROR && $e->errorInfo[1] == Connection::UNKNOWN_CHARSET)) {
$this->fail(t('Your MySQL server and PHP MySQL driver must support utf8mb4 character encoding. Make sure to use a database system that supports this (such as MySQL/MariaDB/Percona 5.5.3 and up), and that the utf8mb4 character set is compiled in. See the <a href=":documentation" target="_blank">MySQL documentation</a> for more information.', [':documentation' => 'https://dev.mysql.com/doc/refman/5.0/en/cannot-initialize-character-set.html']));
$info = Database::getConnectionInfo();
@@ -108,6 +108,16 @@ class Tasks extends InstallTasks {
// Now, attempt the connection again; if it's successful, attempt to
// create the database.
Database::getConnection()->createDatabase($database);
+ Database::closeConnection();
+
+ // Now, restore the database config.
+ Database::removeConnection('default');
+ $connection_info['default']['database'] = $database;
+ Database::addConnectionInfo('default', 'default', $connection_info['default']);
+
+ // Check the database connection.
+ Database::getConnection();
+ $this->pass('Drupal can CONNECT to the database ok.');
}
catch (DatabaseNotFoundException $e) {
// Still no dice; probably a permission issue. Raise the error to the
diff --git a/core/lib/Drupal/Core/Database/Driver/mysql/Schema.php b/core/lib/Drupal/Core/Database/Driver/mysql/Schema.php
index 878607c..9766cfa 100644
--- a/core/lib/Drupal/Core/Database/Driver/mysql/Schema.php
+++ b/core/lib/Drupal/Core/Database/Driver/mysql/Schema.php
@@ -108,6 +108,9 @@ class Schema extends DatabaseSchema {
}
// Process keys & indexes.
+ if (!empty($table['primary key']) && is_array($table['primary key'])) {
+ $this->ensureNotNullPrimaryKey($table['primary key'], $table['fields']);
+ }
$keys = $this->createKeysSql($table);
if (count($keys)) {
$sql .= implode(", \n", $keys) . ", \n";
@@ -118,8 +121,9 @@ class Schema extends DatabaseSchema {
$sql .= 'ENGINE = ' . $table['mysql_engine'] . ' DEFAULT CHARACTER SET ' . $table['mysql_character_set'];
// By default, MySQL uses the default collation for new tables, which is
- // 'utf8mb4_general_ci' for utf8mb4. If an alternate collation has been
- // set, it needs to be explicitly specified.
+ // 'utf8mb4_general_ci' (MySQL 5) or 'utf8mb4_0900_ai_ci' (MySQL 8) for
+ // utf8mb4. If an alternate collation has been set, it needs to be
+ // explicitly specified.
// @see \Drupal\Core\Database\Driver\mysql\Schema
if (!empty($info['collation'])) {
$sql .= ' COLLATE ' . $info['collation'];
@@ -151,12 +155,15 @@ class Schema extends DatabaseSchema {
if (isset($spec['length'])) {
$sql .= '(' . $spec['length'] . ')';
}
+ if (isset($spec['type']) && $spec['type'] == 'varchar_ascii') {
+ $sql .= ' CHARACTER SET ascii';
+ }
if (!empty($spec['binary'])) {
$sql .= ' BINARY';
}
// Note we check for the "type" key here. "mysql_type" is VARCHAR:
- if (isset($spec['type']) && $spec['type'] == 'varchar_ascii') {
- $sql .= ' CHARACTER SET ascii COLLATE ascii_general_ci';
+ elseif (isset($spec['type']) && $spec['type'] == 'varchar_ascii') {
+ $sql .= ' COLLATE ascii_general_ci';
}
}
elseif (isset($spec['precision']) && isset($spec['scale'])) {
@@ -212,7 +219,7 @@ class Schema extends DatabaseSchema {
// Set the correct database-engine specific datatype.
// In case one is already provided, force it to uppercase.
if (isset($field['mysql_type'])) {
- $field['mysql_type'] = Unicode::strtoupper($field['mysql_type']);
+ $field['mysql_type'] = mb_strtoupper($field['mysql_type']);
}
else {
$map = $this->getFieldTypeMap();
@@ -409,6 +416,9 @@ class Schema extends DatabaseSchema {
// Fields that are part of a PRIMARY KEY must be added as NOT NULL.
$is_primary_key = isset($keys_new['primary key']) && in_array($field, $keys_new['primary key'], TRUE);
+ if ($is_primary_key) {
+ $this->ensureNotNullPrimaryKey($keys_new['primary key'], [$field => $spec]);
+ }
$fixnull = FALSE;
if (!empty($spec['not null']) && !isset($spec['default']) && !$is_primary_key) {
@@ -428,14 +438,22 @@ class Schema extends DatabaseSchema {
$query .= ', ADD ' . implode(', ADD ', $keys_sql);
}
$this->connection->query($query);
- if (isset($spec['initial'])) {
+ if (isset($spec['initial_from_field'])) {
+ if (isset($spec['initial'])) {
+ $expression = 'COALESCE(' . $spec['initial_from_field'] . ', :default_initial_value)';
+ $arguments = [':default_initial_value' => $spec['initial']];
+ }
+ else {
+ $expression = $spec['initial_from_field'];
+ $arguments = [];
+ }
$this->connection->update($table)
- ->fields([$field => $spec['initial']])
+ ->expression($field, $expression, $arguments)
->execute();
}
- if (isset($spec['initial_from_field'])) {
+ elseif (isset($spec['initial'])) {
$this->connection->update($table)
- ->expression($field, $spec['initial_from_field'])
+ ->fields([$field => $spec['initial']])
->execute();
}
if ($fixnull) {
@@ -452,6 +470,18 @@ class Schema extends DatabaseSchema {
return FALSE;
}
+ // When dropping a field that is part of a composite primary key MySQL
+ // automatically removes the field from the primary key, which can leave the
+ // table in an invalid state. MariaDB 10.2.8 requires explicitly dropping
+ // the primary key first for this reason. We perform this deletion
+ // explicitly which also makes the behavior on both MySQL and MariaDB
+ // consistent with PostgreSQL.
+ // @see https://mariadb.com/kb/en/library/alter-table
+ $primary_key = $this->findPrimaryKeyColumns($table);
+ if ((count($primary_key) > 1) && in_array($field, $primary_key, TRUE)) {
+ $this->dropPrimaryKey($table);
+ }
+
$this->connection->query('ALTER TABLE {' . $table . '} DROP `' . $field . '`');
return TRUE;
}
@@ -517,6 +547,17 @@ class Schema extends DatabaseSchema {
/**
* {@inheritdoc}
*/
+ protected function findPrimaryKeyColumns($table) {
+ if (!$this->tableExists($table)) {
+ return FALSE;
+ }
+ $result = $this->connection->query("SHOW KEYS FROM {" . $table . "} WHERE Key_name = 'PRIMARY'")->fetchAllAssoc('Column_name');
+ return array_keys($result);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
public function addUniqueKey($table, $name, $fields) {
if (!$this->tableExists($table)) {
throw new SchemaObjectDoesNotExistException(t("Cannot add unique key @name to table @table: table doesn't exist.", ['@table' => $table, '@name' => $name]));
@@ -579,6 +620,9 @@ class Schema extends DatabaseSchema {
if (($field != $field_new) && $this->fieldExists($table, $field_new)) {
throw new SchemaObjectExistsException(t("Cannot rename field @table.@name to @name_new: target field already exists.", ['@table' => $table, '@name' => $field, '@name_new' => $field_new]));
}
+ if (isset($keys_new['primary key']) && in_array($field_new, $keys_new['primary key'], TRUE)) {
+ $this->ensureNotNullPrimaryKey($keys_new['primary key'], [$field_new => $spec]);
+ }
$sql = 'ALTER TABLE {' . $table . '} CHANGE `' . $field . '` ' . $this->createFieldSql($field_new, $this->processField($spec));
if ($keys_sql = $this->createKeysSql($keys_new)) {
@@ -610,11 +654,11 @@ class Schema extends DatabaseSchema {
$condition->condition('column_name', $column);
$condition->compile($this->connection, $this);
// Don't use {} around information_schema.columns table.
- return $this->connection->query("SELECT column_comment FROM information_schema.columns WHERE " . (string) $condition, $condition->arguments())->fetchField();
+ return $this->connection->query("SELECT column_comment as column_comment FROM information_schema.columns WHERE " . (string) $condition, $condition->arguments())->fetchField();
}
$condition->compile($this->connection, $this);
// Don't use {} around information_schema.tables table.
- $comment = $this->connection->query("SELECT table_comment FROM information_schema.tables WHERE " . (string) $condition, $condition->arguments())->fetchField();
+ $comment = $this->connection->query("SELECT table_comment as table_comment FROM information_schema.tables WHERE " . (string) $condition, $condition->arguments())->fetchField();
// Work-around for MySQL 5.0 bug http://bugs.mysql.com/bug.php?id=11379
return preg_replace('/; InnoDB free:.*$/', '', $comment);
}
diff --git a/core/lib/Drupal/Core/Database/Driver/mysql/Upsert.php b/core/lib/Drupal/Core/Database/Driver/mysql/Upsert.php
index 6c1af1e..8eda775 100644
--- a/core/lib/Drupal/Core/Database/Driver/mysql/Upsert.php
+++ b/core/lib/Drupal/Core/Database/Driver/mysql/Upsert.php
@@ -18,6 +18,9 @@ class Upsert extends QueryUpsert {
// Default fields are always placed first for consistency.
$insert_fields = array_merge($this->defaultFields, $this->insertFields);
+ $insert_fields = array_map(function ($field) {
+ return $this->connection->escapeField($field);
+ }, $insert_fields);
$query = $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES ';
diff --git a/core/lib/Drupal/Core/Database/Driver/pgsql/Connection.php b/core/lib/Drupal/Core/Database/Driver/pgsql/Connection.php
index e696f24..80b3089 100644
--- a/core/lib/Drupal/Core/Database/Driver/pgsql/Connection.php
+++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Connection.php
@@ -38,7 +38,7 @@ class Connection extends DatabaseConnection {
/**
* A map of condition operators to PostgreSQL operators.
*
- * In PostgreSQL, 'LIKE' is case-sensitive. ILKE should be used for
+ * In PostgreSQL, 'LIKE' is case-sensitive. ILIKE should be used for
* case-insensitive statements.
*/
protected static $postgresqlConditionOperatorMap = [
diff --git a/core/lib/Drupal/Core/Database/Driver/pgsql/Install/Tasks.php b/core/lib/Drupal/Core/Database/Driver/pgsql/Install/Tasks.php
index 70d9a33..e3312d2 100644
--- a/core/lib/Drupal/Core/Database/Driver/pgsql/Install/Tasks.php
+++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Install/Tasks.php
@@ -58,7 +58,7 @@ class Tasks extends InstallTasks {
protected function connect() {
try {
// This doesn't actually test the connection.
- db_set_active();
+ Database::setActiveConnection();
// Now actually do a check.
Database::getConnection();
$this->pass('Drupal can CONNECT to the database ok.');
diff --git a/core/lib/Drupal/Core/Database/Driver/pgsql/Schema.php b/core/lib/Drupal/Core/Database/Driver/pgsql/Schema.php
index 9e5e1d9..8d01d5e 100644
--- a/core/lib/Drupal/Core/Database/Driver/pgsql/Schema.php
+++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Schema.php
@@ -2,7 +2,6 @@
namespace Drupal\Core\Database\Driver\pgsql;
-use Drupal\Component\Utility\Unicode;
use Drupal\Core\Database\SchemaObjectExistsException;
use Drupal\Core\Database\SchemaObjectDoesNotExistException;
use Drupal\Core\Database\Schema as DatabaseSchema;
@@ -250,7 +249,8 @@ EOD;
}
$sql_keys = [];
- if (isset($table['primary key']) && is_array($table['primary key'])) {
+ if (!empty($table['primary key']) && is_array($table['primary key'])) {
+ $this->ensureNotNullPrimaryKey($table['primary key'], $table['fields']);
$sql_keys[] = 'CONSTRAINT ' . $this->ensureIdentifiersLength($name, '', 'pkey') . ' PRIMARY KEY (' . $this->createPrimaryKeySql($table['primary key']) . ')';
}
if (isset($table['unique keys']) && is_array($table['unique keys'])) {
@@ -350,7 +350,7 @@ EOD;
// Set the correct database-engine specific datatype.
// In case one is already provided, force it to lowercase.
if (isset($field['pgsql_type'])) {
- $field['pgsql_type'] = Unicode::strtolower($field['pgsql_type']);
+ $field['pgsql_type'] = mb_strtolower($field['pgsql_type']);
}
else {
$map = $this->getFieldTypeMap();
@@ -358,7 +358,7 @@ EOD;
}
if (!empty($field['unsigned'])) {
- // Unsigned datatypes are not supported in PostgreSQL 9.1. In MySQL,
+ // Unsigned data types are not supported in PostgreSQL 9.1. In MySQL,
// they are used to ensure a positive number is inserted and it also
// doubles the maximum integer size that can be stored in a field.
// The PostgreSQL schema in Drupal creates a check constraint
@@ -552,7 +552,10 @@ EOD;
}
// Fields that are part of a PRIMARY KEY must be added as NOT NULL.
- $is_primary_key = isset($keys_new['primary key']) && in_array($field, $keys_new['primary key'], TRUE);
+ $is_primary_key = isset($new_keys['primary key']) && in_array($field, $new_keys['primary key'], TRUE);
+ if ($is_primary_key) {
+ $this->ensureNotNullPrimaryKey($new_keys['primary key'], [$field => $spec]);
+ }
$fixnull = FALSE;
if (!empty($spec['not null']) && !isset($spec['default']) && !$is_primary_key) {
@@ -562,14 +565,22 @@ EOD;
$query = 'ALTER TABLE {' . $table . '} ADD COLUMN ';
$query .= $this->createFieldSql($field, $this->processField($spec));
$this->connection->query($query);
- if (isset($spec['initial'])) {
+ if (isset($spec['initial_from_field'])) {
+ if (isset($spec['initial'])) {
+ $expression = 'COALESCE(' . $spec['initial_from_field'] . ', :default_initial_value)';
+ $arguments = [':default_initial_value' => $spec['initial']];
+ }
+ else {
+ $expression = $spec['initial_from_field'];
+ $arguments = [];
+ }
$this->connection->update($table)
- ->fields([$field => $spec['initial']])
+ ->expression($field, $expression, $arguments)
->execute();
}
- if (isset($spec['initial_from_field'])) {
+ elseif (isset($spec['initial'])) {
$this->connection->update($table)
- ->expression($field, $spec['initial_from_field'])
+ ->fields([$field => $spec['initial']])
->execute();
}
if ($fixnull) {
@@ -631,6 +642,15 @@ EOD;
/**
* {@inheritdoc}
*/
+ public function fieldExists($table, $column) {
+ $prefixInfo = $this->getPrefixInfo($table);
+
+ return (bool) $this->connection->query("SELECT 1 FROM pg_attribute WHERE attrelid = :key::regclass AND attname = :column AND NOT attisdropped AND attnum > 0", [':key' => $prefixInfo['schema'] . '.' . $prefixInfo['table'], ':column' => $column])->fetchField();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
public function indexExists($table, $name) {
// Details http://www.postgresql.org/docs/9.1/interactive/view-pg-indexes.html
$index_name = $this->ensureIdentifiersLength($table, $name, 'idx');
@@ -702,6 +722,29 @@ EOD;
/**
* {@inheritdoc}
*/
+ protected function findPrimaryKeyColumns($table) {
+ if (!$this->tableExists($table)) {
+ return FALSE;
+ }
+
+ // Fetch the 'indkey' column from 'pg_index' to figure out the order of the
+ // primary key.
+ // @todo Use 'array_position()' to be able to perform the ordering in SQL
+ // directly when 9.5 is the minimum PostgreSQL version.
+ $result = $this->connection->query("SELECT a.attname, i.indkey FROM pg_index i JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey) WHERE i.indrelid = '{" . $table . "}'::regclass AND i.indisprimary")->fetchAllKeyed();
+ if (!$result) {
+ return [];
+ }
+
+ $order = explode(' ', reset($result));
+ $columns = array_combine($order, array_keys($result));
+ ksort($columns);
+ return array_values($columns);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
public function addUniqueKey($table, $name, $fields) {
if (!$this->tableExists($table)) {
throw new SchemaObjectDoesNotExistException(t("Cannot add unique key @name to table @table: table doesn't exist.", ['@table' => $table, '@name' => $name]));
@@ -765,6 +808,9 @@ EOD;
if (($field != $field_new) && $this->fieldExists($table, $field_new)) {
throw new SchemaObjectExistsException(t("Cannot rename field @table.@name to @name_new: target field already exists.", ['@table' => $table, '@name' => $field, '@name_new' => $field_new]));
}
+ if (isset($new_keys['primary key']) && in_array($field_new, $new_keys['primary key'], TRUE)) {
+ $this->ensureNotNullPrimaryKey($new_keys['primary key'], [$field_new => $spec]);
+ }
$spec = $this->processField($spec);
diff --git a/core/lib/Drupal/Core/Database/Driver/sqlite/Connection.php b/core/lib/Drupal/Core/Database/Driver/sqlite/Connection.php
index a7c1496..ef0dd86 100644
--- a/core/lib/Drupal/Core/Database/Driver/sqlite/Connection.php
+++ b/core/lib/Drupal/Core/Database/Driver/sqlite/Connection.php
@@ -165,7 +165,6 @@ class Connection extends DatabaseConnection {
return $pdo;
}
-
/**
* Destructor for the SQLite connection.
*
@@ -436,4 +435,50 @@ class Connection extends DatabaseConnection {
return $prefix . $table;
}
+ /**
+ * {@inheritdoc}
+ */
+ public static function createConnectionOptionsFromUrl($url, $root) {
+ $database = parent::createConnectionOptionsFromUrl($url, $root);
+
+ // A SQLite database path with two leading slashes indicates a system path.
+ // Otherwise the path is relative to the Drupal root.
+ $url_components = parse_url($url);
+ if ($url_components['path'][0] === '/') {
+ $url_components['path'] = substr($url_components['path'], 1);
+ }
+ if ($url_components['path'][0] === '/') {
+ $database['database'] = $url_components['path'];
+ }
+ else {
+ $database['database'] = $root . '/' . $url_components['path'];
+ }
+
+ // User credentials and system port are irrelevant for SQLite.
+ unset(
+ $database['username'],
+ $database['password'],
+ $database['port']
+ );
+
+ return $database;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function createUrlFromConnectionOptions(array $connection_options) {
+ if (!isset($connection_options['driver'], $connection_options['database'])) {
+ throw new \InvalidArgumentException("As a minimum, the connection options array must contain at least the 'driver' and 'database' keys");
+ }
+
+ $db_url = 'sqlite://localhost/' . $connection_options['database'];
+
+ if (isset($connection_options['prefix']['default']) && $connection_options['prefix']['default'] !== NULL && $connection_options['prefix']['default'] !== '') {
+ $db_url .= '#' . $connection_options['prefix']['default'];
+ }
+
+ return $db_url;
+ }
+
}
diff --git a/core/lib/Drupal/Core/Database/Driver/sqlite/Install/Tasks.php b/core/lib/Drupal/Core/Database/Driver/sqlite/Install/Tasks.php
index b0ea188..f5cb289 100644
--- a/core/lib/Drupal/Core/Database/Driver/sqlite/Install/Tasks.php
+++ b/core/lib/Drupal/Core/Database/Driver/sqlite/Install/Tasks.php
@@ -54,7 +54,7 @@ class Tasks extends InstallTasks {
protected function connect() {
try {
// This doesn't actually test the connection.
- db_set_active();
+ Database::setActiveConnection();
// Now actually do a check.
Database::getConnection();
$this->pass('Drupal can CONNECT to the database ok.');
diff --git a/core/lib/Drupal/Core/Database/Driver/sqlite/Schema.php b/core/lib/Drupal/Core/Database/Driver/sqlite/Schema.php
index 27b6a58..0f0886c 100644
--- a/core/lib/Drupal/Core/Database/Driver/sqlite/Schema.php
+++ b/core/lib/Drupal/Core/Database/Driver/sqlite/Schema.php
@@ -2,7 +2,6 @@
namespace Drupal\Core\Database\Driver\sqlite;
-use Drupal\Component\Utility\Unicode;
use Drupal\Core\Database\SchemaObjectExistsException;
use Drupal\Core\Database\SchemaObjectDoesNotExistException;
use Drupal\Core\Database\Schema as DatabaseSchema;
@@ -19,6 +18,8 @@ class Schema extends DatabaseSchema {
/**
* Override DatabaseSchema::$defaultSchema
+ *
+ * @var string
*/
protected $defaultSchema = 'main';
@@ -51,6 +52,10 @@ class Schema extends DatabaseSchema {
* An array of SQL statements to create the table.
*/
public function createTableSql($name, $table) {
+ if (!empty($table['primary key']) && is_array($table['primary key'])) {
+ $this->ensureNotNullPrimaryKey($table['primary key'], $table['fields']);
+ }
+
$sql = [];
$sql[] = "CREATE TABLE {" . $name . "} (\n" . $this->createColumnsSql($name, $table) . "\n)\n";
return array_merge($sql, $this->createIndexSql($name, $table));
@@ -129,7 +134,7 @@ class Schema extends DatabaseSchema {
// Set the correct database-engine specific datatype.
// In case one is already provided, force it to uppercase.
if (isset($field['sqlite_type'])) {
- $field['sqlite_type'] = Unicode::strtoupper($field['sqlite_type']);
+ $field['sqlite_type'] = mb_strtoupper($field['sqlite_type']);
}
else {
$map = $this->getFieldTypeMap();
@@ -314,6 +319,9 @@ class Schema extends DatabaseSchema {
if ($this->fieldExists($table, $field)) {
throw new SchemaObjectExistsException(t("Cannot add field @table.@field: field already exists.", ['@field' => $field, '@table' => $table]));
}
+ if (isset($keys_new['primary key']) && in_array($field, $keys_new['primary key'], TRUE)) {
+ $this->ensureNotNullPrimaryKey($keys_new['primary key'], [$field => $specification]);
+ }
// SQLite doesn't have a full-featured ALTER TABLE statement. It only
// supports adding new fields to a table, in some simple cases. In most
@@ -325,14 +333,22 @@ class Schema extends DatabaseSchema {
$this->connection->query($query);
// Apply the initial value if set.
- if (isset($specification['initial'])) {
+ if (isset($specification['initial_from_field'])) {
+ if (isset($specification['initial'])) {
+ $expression = 'COALESCE(' . $specification['initial_from_field'] . ', :default_initial_value)';
+ $arguments = [':default_initial_value' => $specification['initial']];
+ }
+ else {
+ $expression = $specification['initial_from_field'];
+ $arguments = [];
+ }
$this->connection->update($table)
- ->fields([$field => $specification['initial']])
+ ->expression($field, $expression, $arguments)
->execute();
}
- if (isset($specification['initial_from_field'])) {
+ elseif (isset($specification['initial'])) {
$this->connection->update($table)
- ->expression($field, $specification['initial_from_field'])
+ ->fields([$field => $specification['initial']])
->execute();
}
}
@@ -347,18 +363,26 @@ class Schema extends DatabaseSchema {
// Build the mapping between the old fields and the new fields.
$mapping = [];
- if (isset($specification['initial'])) {
+ if (isset($specification['initial_from_field'])) {
// If we have a initial value, copy it over.
+ if (isset($specification['initial'])) {
+ $expression = 'COALESCE(' . $specification['initial_from_field'] . ', :default_initial_value)';
+ $arguments = [':default_initial_value' => $specification['initial']];
+ }
+ else {
+ $expression = $specification['initial_from_field'];
+ $arguments = [];
+ }
$mapping[$field] = [
- 'expression' => ':newfieldinitial',
- 'arguments' => [':newfieldinitial' => $specification['initial']],
+ 'expression' => $expression,
+ 'arguments' => $arguments,
];
}
- elseif (isset($specification['initial_from_field'])) {
+ elseif (isset($specification['initial'])) {
// If we have a initial value, copy it over.
$mapping[$field] = [
- 'expression' => $specification['initial_from_field'],
- 'arguments' => [],
+ 'expression' => ':newfieldinitial',
+ 'arguments' => [':newfieldinitial' => $specification['initial']],
];
}
else {
@@ -411,7 +435,7 @@ class Schema extends DatabaseSchema {
// Now add the fields.
foreach ($mapping as $field_alias => $field_source) {
- // Just ignore this field (ie. use it's default value).
+ // Just ignore this field (ie. use its default value).
if (!isset($field_source)) {
continue;
}
@@ -448,7 +472,7 @@ class Schema extends DatabaseSchema {
* Name of the table.
*
* @return
- * An array representing the schema, from drupal_get_schema().
+ * An array representing the schema.
*
* @throws \Exception
* If a column of the table could not be parsed.
@@ -478,20 +502,27 @@ class Schema extends DatabaseSchema {
$schema['fields'][$row->name] = [
'type' => $type,
'size' => $size,
- 'not null' => !empty($row->notnull),
+ 'not null' => !empty($row->notnull) || $row->pk !== "0",
'default' => trim($row->dflt_value, "'"),
];
if ($length) {
$schema['fields'][$row->name]['length'] = $length;
}
+ // $row->pk contains a number that reflects the primary key order. We
+ // use that as the key and sort (by key) below to return the primary key
+ // in the same order that it is stored in.
if ($row->pk) {
- $schema['primary key'][] = $row->name;
+ $schema['primary key'][$row->pk] = $row->name;
}
}
else {
throw new \Exception("Unable to parse the column type " . $row->type);
}
}
+ ksort($schema['primary key']);
+ // Re-key the array because $row->pk starts counting at 1.
+ $schema['primary key'] = array_values($schema['primary key']);
+
$indexes = [];
$result = $this->connection->query('PRAGMA ' . $info['schema'] . '.index_list(' . $info['table'] . ')');
foreach ($result as $row) {
@@ -527,9 +558,11 @@ class Schema extends DatabaseSchema {
unset($new_schema['fields'][$field]);
- // Handle possible primary key changes.
- if (isset($new_schema['primary key']) && ($key = array_search($field, $new_schema['primary key'])) !== FALSE) {
- unset($new_schema['primary key'][$key]);
+ // Drop the primary key if the field to drop is part of it. This is
+ // consistent with the behavior on PostgreSQL.
+ // @see \Drupal\Core\Database\Driver\mysql\Schema::dropField()
+ if (isset($new_schema['primary key']) && in_array($field, $new_schema['primary key'], TRUE)) {
+ unset($new_schema['primary key']);
}
// Handle possible index changes.
@@ -558,6 +591,9 @@ class Schema extends DatabaseSchema {
if (($field != $field_new) && $this->fieldExists($table, $field_new)) {
throw new SchemaObjectExistsException(t("Cannot rename field @table.@name to @name_new: target field already exists.", ['@table' => $table, '@name' => $field, '@name_new' => $field_new]));
}
+ if (isset($keys_new['primary key']) && in_array($field_new, $keys_new['primary key'], TRUE)) {
+ $this->ensureNotNullPrimaryKey($keys_new['primary key'], [$field_new => $spec]);
+ }
$old_schema = $this->introspectSchema($table);
$new_schema = $old_schema;
@@ -609,8 +645,10 @@ class Schema extends DatabaseSchema {
if (is_array($field)) {
$field = &$field[0];
}
- if (isset($mapping[$field])) {
- $field = $mapping[$field];
+
+ $mapped_field = array_search($field, $mapping, TRUE);
+ if ($mapped_field !== FALSE) {
+ $field = $mapped_field;
}
}
return $key_definition;
@@ -705,6 +743,7 @@ class Schema extends DatabaseSchema {
}
$new_schema['primary key'] = $fields;
+ $this->ensureNotNullPrimaryKey($new_schema['primary key'], $new_schema['fields']);
$this->alterTable($table, $old_schema, $new_schema);
}
@@ -727,6 +766,17 @@ class Schema extends DatabaseSchema {
/**
* {@inheritdoc}
*/
+ protected function findPrimaryKeyColumns($table) {
+ if (!$this->tableExists($table)) {
+ return FALSE;
+ }
+ $schema = $this->introspectSchema($table);
+ return $schema['primary key'];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
public function fieldSetDefault($table, $field, $default) {
if (!$this->fieldExists($table, $field)) {
throw new SchemaObjectDoesNotExistException(t("Cannot set default value of field @table.@field: field doesn't exist.", ['@table' => $table, '@field' => $field]));
diff --git a/core/lib/Drupal/Core/Database/Driver/sqlite/Select.php b/core/lib/Drupal/Core/Database/Driver/sqlite/Select.php
index 6d7caab..5eaa515 100644
--- a/core/lib/Drupal/Core/Database/Driver/sqlite/Select.php
+++ b/core/lib/Drupal/Core/Database/Driver/sqlite/Select.php
@@ -8,6 +8,7 @@ use Drupal\Core\Database\Query\Select as QuerySelect;
* SQLite implementation of \Drupal\Core\Database\Query\Select.
*/
class Select extends QuerySelect {
+
public function forUpdate($set = TRUE) {
// SQLite does not support FOR UPDATE so nothing to do.
return $this;
diff --git a/core/lib/Drupal/Core/Database/Driver/sqlite/Statement.php b/core/lib/Drupal/Core/Database/Driver/sqlite/Statement.php
index 5610d07..d9b422b 100644
--- a/core/lib/Drupal/Core/Database/Driver/sqlite/Statement.php
+++ b/core/lib/Drupal/Core/Database/Driver/sqlite/Statement.php
@@ -27,7 +27,7 @@ class Statement extends StatementPrefetch implements StatementInterface {
* See http://bugs.php.net/bug.php?id=45259 for more details.
*/
protected function getStatement($query, &$args = []) {
- if (count($args)) {
+ if (is_array($args) && !empty($args)) {
// Check if $args is a simple numeric array.
if (range(0, count($args) - 1) === array_keys($args)) {
// In that case, we have unnamed placeholders.
diff --git a/core/lib/Drupal/Core/Database/Driver/sqlite/Truncate.php b/core/lib/Drupal/Core/Database/Driver/sqlite/Truncate.php
index c58ff7d..386912f 100644
--- a/core/lib/Drupal/Core/Database/Driver/sqlite/Truncate.php
+++ b/core/lib/Drupal/Core/Database/Driver/sqlite/Truncate.php
@@ -11,6 +11,7 @@ use Drupal\Core\Database\Query\Truncate as QueryTruncate;
* exactly the effect (it is implemented by DROPing the table).
*/
class Truncate extends QueryTruncate {
+
public function __toString() {
// Create a sanitized comment string to prepend to the query.
$comments = $this->connection->makeComment($this->comments);
diff --git a/core/lib/Drupal/Core/Database/Install/Tasks.php b/core/lib/Drupal/Core/Database/Install/Tasks.php
index a2ea41b..90a0772 100644
--- a/core/lib/Drupal/Core/Database/Install/Tasks.php
+++ b/core/lib/Drupal/Core/Database/Install/Tasks.php
@@ -33,7 +33,7 @@ abstract class Tasks {
],
[
'arguments' => [
- 'CREATE TABLE {drupal_install_test} (id int NULL)',
+ 'CREATE TABLE {drupal_install_test} (id int NOT NULL PRIMARY KEY)',
'Drupal can use CREATE TABLE database commands.',
'Failed to <strong>CREATE</strong> a test table on your database server with the command %query. The server reports the following message: %error.<p>Are you sure the configured username has the necessary permissions to create tables in the database?</p>',
TRUE,
@@ -156,7 +156,7 @@ abstract class Tasks {
protected function connect() {
try {
// This doesn't actually test the connection.
- db_set_active();
+ Database::setActiveConnection();
// Now actually do a check.
Database::getConnection();
$this->pass('Drupal can CONNECT to the database ok.');
diff --git a/core/lib/Drupal/Core/Database/Query/Condition.php b/core/lib/Drupal/Core/Database/Query/Condition.php
index 5169357..dd9a730 100644
--- a/core/lib/Drupal/Core/Database/Query/Condition.php
+++ b/core/lib/Drupal/Core/Database/Query/Condition.php
@@ -375,7 +375,8 @@ class Condition implements ConditionInterface, \Countable {
}
else {
// We need to upper case because PHP index matches are case sensitive but
- // do not need the more expensive Unicode::strtoupper() because SQL statements are ASCII.
+ // do not need the more expensive mb_strtoupper() because SQL statements
+ // are ASCII.
$operator = strtoupper($operator);
$return = isset(static::$conditionOperatorMap[$operator]) ? static::$conditionOperatorMap[$operator] : [];
}
diff --git a/core/lib/Drupal/Core/Database/Query/Merge.php b/core/lib/Drupal/Core/Database/Query/Merge.php
index 43188ec..6a4a5ca 100644
--- a/core/lib/Drupal/Core/Database/Query/Merge.php
+++ b/core/lib/Drupal/Core/Database/Query/Merge.php
@@ -174,7 +174,7 @@ class Merge extends Query implements ConditionInterface {
* Specifies fields to be updated as an expression.
*
* Expression fields are cases such as counter = counter + 1. This method
- * takes precedence over MergeQuery::updateFields() and it's wrappers,
+ * takes precedence over MergeQuery::updateFields() and its wrappers,
* MergeQuery::key() and MergeQuery::fields().
*
* @param $field
diff --git a/core/lib/Drupal/Core/Database/Query/Query.php b/core/lib/Drupal/Core/Database/Query/Query.php
index 94f71a5..46c8173 100644
--- a/core/lib/Drupal/Core/Database/Query/Query.php
+++ b/core/lib/Drupal/Core/Database/Query/Query.php
@@ -48,6 +48,8 @@ abstract class Query implements PlaceholderInterface {
/**
* The placeholder counter.
+ *
+ * @var int
*/
protected $nextPlaceholder = 0;
diff --git a/core/lib/Drupal/Core/Database/Query/Select.php b/core/lib/Drupal/Core/Database/Query/Select.php
index ee4ed84..fe88f84 100644
--- a/core/lib/Drupal/Core/Database/Query/Select.php
+++ b/core/lib/Drupal/Core/Database/Query/Select.php
@@ -113,6 +113,8 @@ class Select extends Query implements SelectInterface {
/**
* The FOR UPDATE status
+ *
+ * @var bool
*/
protected $forUpdate = FALSE;
@@ -826,7 +828,7 @@ class Select extends Query implements SelectInterface {
$query .= implode(', ', $fields);
// FROM - We presume all queries have a FROM, as any query that doesn't won't need the query builder anyway.
- $query .= "\nFROM ";
+ $query .= "\nFROM";
foreach ($this->tables as $table) {
$query .= "\n";
if (isset($table['join type'])) {
@@ -912,6 +914,8 @@ class Select extends Query implements SelectInterface {
* {@inheritdoc}
*/
public function __clone() {
+ parent::__clone();
+
// On cloning, also clone the dependent objects. However, we do not
// want to clone the database connection object as that would duplicate the
// connection itself.
@@ -921,6 +925,11 @@ class Select extends Query implements SelectInterface {
foreach ($this->union as $key => $aggregate) {
$this->union[$key]['query'] = clone($aggregate['query']);
}
+ foreach ($this->tables as $alias => $table) {
+ if ($table['table'] instanceof SelectInterface) {
+ $this->tables[$alias]['table'] = clone $table['table'];
+ }
+ }
}
}
diff --git a/core/lib/Drupal/Core/Database/Query/SelectExtender.php b/core/lib/Drupal/Core/Database/Query/SelectExtender.php
index 9082ca8..5a09b89 100644
--- a/core/lib/Drupal/Core/Database/Query/SelectExtender.php
+++ b/core/lib/Drupal/Core/Database/Query/SelectExtender.php
@@ -30,6 +30,8 @@ class SelectExtender implements SelectInterface {
/**
* The placeholder counter.
+ *
+ * @var int
*/
protected $placeholder = 0;
diff --git a/core/lib/Drupal/Core/Database/Query/SelectInterface.php b/core/lib/Drupal/Core/Database/Query/SelectInterface.php
index 59b15d0..e740b74 100644
--- a/core/lib/Drupal/Core/Database/Query/SelectInterface.php
+++ b/core/lib/Drupal/Core/Database/Query/SelectInterface.php
@@ -95,7 +95,7 @@ interface SelectInterface extends ConditionInterface, AlterableInterface, Extend
* Note that this method must be called by reference as well:
*
* @code
- * $fields =& $query->getTables();
+ * $tables =& $query->getTables();
* @endcode
*
* @return
@@ -346,6 +346,8 @@ interface SelectInterface extends ConditionInterface, AlterableInterface, Extend
* db_query('A')->rightJoin('B') is identical to
* db_query('B')->leftJoin('A'). This functionality has been deprecated
* because SQLite does not support it.
+ *
+ * @see https://www.drupal.org/node/2765249
*/
public function rightJoin($table, $alias = NULL, $condition = NULL, $arguments = []);
@@ -402,10 +404,10 @@ interface SelectInterface extends ConditionInterface, AlterableInterface, Extend
* on.
*
* Example:
- * <code>
+ * @code
* $query->addExpression('SUBSTRING(thread, 1, (LENGTH(thread) - 1))', 'order_field');
* $query->orderBy('order_field', 'ASC');
- * </code>
+ * @endcode
* @param $direction
* The direction to sort. Legal values are "ASC" and "DESC". Any other value
* will be converted to "ASC".
diff --git a/core/lib/Drupal/Core/Database/Schema.php b/core/lib/Drupal/Core/Database/Schema.php
index 80a68f6..4bd0a9b 100644
--- a/core/lib/Drupal/Core/Database/Schema.php
+++ b/core/lib/Drupal/Core/Database/Schema.php
@@ -19,6 +19,8 @@ abstract class Schema implements PlaceholderInterface {
/**
* The placeholder counter.
+ *
+ * @var int
*/
protected $placeholder = 0;
@@ -30,6 +32,8 @@ abstract class Schema implements PlaceholderInterface {
* method.
*
* @see DatabaseSchema::getPrefixInfo()
+ *
+ * @var string
*/
protected $defaultSchema = 'public';
@@ -195,7 +199,7 @@ abstract class Schema implements PlaceholderInterface {
// couldn't use db_select() here because it would prefix
// information_schema.tables and the query would fail.
// Don't use {} around information_schema.tables table.
- $results = $this->connection->query("SELECT table_name FROM information_schema.tables WHERE " . (string) $condition, $condition->arguments());
+ $results = $this->connection->query("SELECT table_name as table_name FROM information_schema.tables WHERE " . (string) $condition, $condition->arguments());
foreach ($results as $table) {
// Take into account tables that have an individual prefix.
if (isset($individually_prefixed_tables[$table->table_name])) {
@@ -405,6 +409,26 @@ abstract class Schema implements PlaceholderInterface {
abstract public function dropPrimaryKey($table);
/**
+ * Finds the primary key columns of a table, from the database.
+ *
+ * @param string $table
+ * The name of the table.
+ *
+ * @return string[]|false
+ * A simple array with the names of the columns composing the table's
+ * primary key, or FALSE if the table does not exist.
+ *
+ * @throws \RuntimeException
+ * If the driver does not override this method.
+ */
+ protected function findPrimaryKeyColumns($table) {
+ if (!$this->tableExists($table)) {
+ return FALSE;
+ }
+ throw new \RuntimeException("The '" . $this->connection->driver() . "' database driver does not implement " . __METHOD__);
+ }
+
+ /**
* Add a unique key.
*
* @param $table
@@ -658,4 +682,25 @@ abstract class Schema implements PlaceholderInterface {
return is_string($value) ? $this->connection->quote($value) : $value;
}
+ /**
+ * Ensures that all the primary key fields are correctly defined.
+ *
+ * @param array $primary_key
+ * An array containing the fields that will form the primary key of a table.
+ * @param array $fields
+ * An array containing the field specifications of the table, as per the
+ * schema data structure format.
+ *
+ * @throws \Drupal\Core\Database\SchemaException
+ * Thrown if any primary key field specification does not exist or if they
+ * do not define 'not null' as TRUE.
+ */
+ protected function ensureNotNullPrimaryKey(array $primary_key, array $fields) {
+ foreach (array_intersect($primary_key, array_keys($fields)) as $field_name) {
+ if (!isset($fields[$field_name]['not null']) || $fields[$field_name]['not null'] !== TRUE) {
+ throw new SchemaException("The '$field_name' field specification does not define 'not null' as TRUE.");
+ }
+ }
+ }
+
}
diff --git a/core/lib/Drupal/Core/Database/StatementInterface.php b/core/lib/Drupal/Core/Database/StatementInterface.php
index a97043a..4f4248d 100644
--- a/core/lib/Drupal/Core/Database/StatementInterface.php
+++ b/core/lib/Drupal/Core/Database/StatementInterface.php
@@ -44,7 +44,7 @@ interface StatementInterface extends \Traversable {
*
* @param $args
* An array of values with as many elements as there are bound parameters in
- * the SQL statement being executed.
+ * the SQL statement being executed. This can be NULL.
* @param $options
* An array of options for this query.
*
diff --git a/core/lib/Drupal/Core/Database/StatementPrefetch.php b/core/lib/Drupal/Core/Database/StatementPrefetch.php
index 4e940ea..fb7b0b4 100644
--- a/core/lib/Drupal/Core/Database/StatementPrefetch.php
+++ b/core/lib/Drupal/Core/Database/StatementPrefetch.php
@@ -20,7 +20,7 @@ class StatementPrefetch implements \Iterator, StatementInterface {
/**
* Driver-specific options. Can be used by child classes.
*
- * @var Array
+ * @var array
*/
protected $driverOptions;
@@ -41,14 +41,14 @@ class StatementPrefetch implements \Iterator, StatementInterface {
/**
* Main data store.
*
- * @var Array
+ * @var array
*/
protected $data = [];
/**
* The current row, retrieved in \PDO::FETCH_ASSOC format.
*
- * @var Array
+ * @var array
*/
protected $currentRow = NULL;
@@ -62,7 +62,7 @@ class StatementPrefetch implements \Iterator, StatementInterface {
/**
* The list of column names in this result set.
*
- * @var Array
+ * @var array
*/
protected $columnNames = NULL;
@@ -91,7 +91,7 @@ class StatementPrefetch implements \Iterator, StatementInterface {
/**
* Holds supplementary current fetch options (which will be used by the next fetch).
*
- * @var Array
+ * @var array
*/
protected $fetchOptions = [
'class' => 'stdClass',
@@ -110,7 +110,7 @@ class StatementPrefetch implements \Iterator, StatementInterface {
/**
* Holds supplementary default fetch options.
*
- * @var Array
+ * @var array
*/
protected $defaultFetchOptions = [
'class' => 'stdClass',
@@ -214,8 +214,8 @@ class StatementPrefetch implements \Iterator, StatementInterface {
*
* @param $query
* The query.
- * @param array $args
- * An array of arguments.
+ * @param array|null $args
+ * An array of arguments. This can be NULL.
* @return \PDOStatement
* A PDOStatement object.
*/
diff --git a/core/lib/Drupal/Core/Database/database.api.php b/core/lib/Drupal/Core/Database/database.api.php
index 45a5618..c39cf78 100644
--- a/core/lib/Drupal/Core/Database/database.api.php
+++ b/core/lib/Drupal/Core/Database/database.api.php
@@ -22,7 +22,7 @@ use Drupal\Core\Database\Query\Condition;
* practices.
*
* For more detailed information on the database abstraction layer, see
- * https://www.drupal.org/developing/api/database.
+ * https://www.drupal.org/docs/8/api/database-api/database-api-overview.
*
* @section sec_entity Querying entities
* Any query on Drupal entities or fields should use the Entity Query API. See
@@ -30,19 +30,20 @@ use Drupal\Core\Database\Query\Condition;
*
* @section sec_simple Simple SELECT database queries
* For simple SELECT queries that do not involve entities, the Drupal database
- * abstraction layer provides the functions db_query() and db_query_range(),
- * which execute SELECT queries (optionally with range limits) and return result
- * sets that you can iterate over using foreach loops. (The result sets are
- * objects implementing the \Drupal\Core\Database\StatementInterface interface.)
+ * abstraction layer provides the functions \Drupal::database()->query() and
+ * \Drupal::database()->queryRange(), which execute SELECT queries (optionally
+ * with range limits) and return result sets that you can iterate over using
+ * foreach loops. (The result sets are objects implementing the
+ * \Drupal\Core\Database\StatementInterface interface.)
* You can use the simple query functions for query strings that are not
* dynamic (except for placeholders, see below), and that you are certain will
* work in any database engine. See @ref sec_dynamic below if you have a more
* complex query, or a query whose syntax would be different in some databases.
*
- * As a note, db_query() and similar functions are wrappers on connection object
- * methods. In most classes, you should use dependency injection and the
- * database connection object instead of these wrappers; See @ref sec_connection
- * below for details.
+ * Note: \Drupal::database() is used here as a shorthand way to get a reference
+ * to the database connection object. In most classes, you should use dependency
+ * injection and inject the 'database' service to perform queries. See
+ * @ref sec_connection below for details.
*
* To use the simple database query functions, you will need to make a couple of
* modifications to your bare SQL query:
@@ -55,7 +56,8 @@ use Drupal\Core\Database\Query\Condition;
* putting variables directly into the query, to protect against SQL
* injection attacks.
* - LIMIT syntax differs between databases, so if you have a ranged query,
- * use db_query_range() instead of db_query().
+ * use \Drupal::database()->queryRange() instead of
+ * \Drupal::database()->query().
*
* For example, if the query you want to run is:
* @code
@@ -64,7 +66,7 @@ use Drupal\Core\Database\Query\Condition;
* @endcode
* you would do it like this:
* @code
- * $result = db_query_range('SELECT e.id, e.title, e.created
+ * $result = \Drupal::database()->queryRange('SELECT e.id, e.title, e.created
* FROM {example} e
* WHERE e.uid = :uid
* ORDER BY e.created DESC',
@@ -91,16 +93,11 @@ use Drupal\Core\Database\Query\Condition;
* fields (see the @link entity_api Entity API topic @endlink for more on
* entity queries).
*
- * As a note, db_select() and similar functions are wrappers on connection
- * object methods. In most classes, you should use dependency injection and the
- * database connection object instead of these wrappers; See @ref sec_connection
- * below for details.
- *
* The dynamic query API lets you build up a query dynamically using method
* calls. As an illustration, the query example from @ref sec_simple above
* would be:
* @code
- * $result = db_select('example', 'e')
+ * $result = \Drupal::database()->select('example', 'e')
* ->fields('e', array('id', 'title', 'created'))
* ->condition('e.uid', $uid)
* ->orderBy('e.created', 'DESC')
@@ -109,7 +106,7 @@ use Drupal\Core\Database\Query\Condition;
* @endcode
*
* There are also methods to join to other tables, add fields with aliases,
- * isNull() to have a @code WHERE e.foo IS NULL @endcode condition, etc. See
+ * isNull() to query for NULL values, etc. See
* https://www.drupal.org/developing/api/database for many more details.
*
* One note on chaining: It is common in the dynamic database API to chain
@@ -123,17 +120,19 @@ use Drupal\Core\Database\Query\Condition;
* returns the query or something else, and only chain methods that return the
* query.
*
- * @section_insert INSERT, UPDATE, and DELETE queries
+ * @section sec_insert INSERT, UPDATE, and DELETE queries
* INSERT, UPDATE, and DELETE queries need special care in order to behave
- * consistently across databases; you should never use db_query() to run
- * an INSERT, UPDATE, or DELETE query. Instead, use functions db_insert(),
- * db_update(), and db_delete() to obtain a base query on your table, and then
- * add dynamic conditions (as illustrated in @ref sec_dynamic above).
- *
- * As a note, db_insert() and similar functions are wrappers on connection
- * object methods. In most classes, you should use dependency injection and the
- * database connection object instead of these wrappers; See @ref sec_connection
- * below for details.
+ * consistently across databases; you should never use
+ * \Drupal::database()->query() to run an INSERT, UPDATE, or DELETE query.
+ * Instead, use functions \Drupal::database()->insert(),
+ * \Drupal::database()->update(), and \Drupal::database()->delete() to obtain
+ * a base query on your table, and then add dynamic conditions (as illustrated
+ * in @ref sec_dynamic above).
+ *
+ * Note: \Drupal::database() is used here as a shorthand way to get a reference
+ * to the database connection object. In most classes, you should use dependency
+ * injection and inject the 'database' service to perform queries. See
+ * @ref sec_connection below for details.
*
* For example, if your query is:
* @code
@@ -142,7 +141,7 @@ use Drupal\Core\Database\Query\Condition;
* You can execute it via:
* @code
* $fields = array('id' => 1, 'uid' => 2, 'path' => 'path', 'name' => 'Name');
- * db_insert('example')
+ * \Drupal::database()->insert('example')
* ->fields($fields)
* ->execute();
* @endcode
@@ -150,21 +149,26 @@ use Drupal\Core\Database\Query\Condition;
* @section sec_transaction Transactions
* Drupal supports transactions, including a transparent fallback for
* databases that do not support transactions. To start a new transaction,
- * call @code $txn = db_transaction(); @endcode The transaction will
- * remain open for as long as the variable $txn remains in scope; when $txn is
- * destroyed, the transaction will be committed. If your transaction is nested
- * inside of another then Drupal will track each transaction and only commit
- * the outer-most transaction when the last transaction object goes out out of
- * scope (when all relevant queries have completed successfully).
+ * call startTransaction(), like this:
+ * @code
+ * $transaction = \Drupal::database()->startTransaction();
+ * @endcode
+ * The transaction will remain open for as long as the variable $transaction
+ * remains in scope; when $transaction is destroyed, the transaction will be
+ * committed. If your transaction is nested inside of another then Drupal will
+ * track each transaction and only commit the outer-most transaction when the
+ * last transaction object goes out out of scope (when all relevant queries have
+ * completed successfully).
*
* Example:
* @code
* function my_transaction_function() {
+ * $connection = \Drupal::database();
* // The transaction opens here.
- * $txn = db_transaction();
+ * $transaction = $connection->startTransaction();
*
* try {
- * $id = db_insert('example')
+ * $id = $connection->insert('example')
* ->fields(array(
* 'field1' => 'mystring',
* 'field2' => 5,
@@ -177,20 +181,21 @@ use Drupal\Core\Database\Query\Condition;
* }
* catch (Exception $e) {
* // Something went wrong somewhere, so roll back now.
- * $txn->rollBack();
+ * $transaction->rollBack();
* // Log the exception to watchdog.
* watchdog_exception('type', $e);
* }
*
- * // $txn goes out of scope here. Unless the transaction was rolled back, it
- * // gets automatically committed here.
+ * // $transaction goes out of scope here. Unless the transaction was rolled
+ * // back, it gets automatically committed here.
* }
*
* function my_other_function($id) {
+ * $connection = \Drupal::database();
* // The transaction is still open here.
*
* if ($id % 2 == 0) {
- * db_update('example')
+ * $connection->update('example')
* ->condition('id', $id)
* ->fields(array('field2' => 10))
* ->execute();
@@ -199,18 +204,19 @@ use Drupal\Core\Database\Query\Condition;
* @endcode
*
* @section sec_connection Database connection objects
- * The examples here all use functions like db_select() and db_query(), which
- * can be called from any Drupal method or function code. In some classes, you
- * may already have a database connection object in a member variable, or it may
- * be passed into a class constructor via dependency injection. If that is the
- * case, you can look at the code for db_select() and the other functions to see
- * how to get a query object from your connection variable. For example:
+ * The examples here all use functions like \Drupal::database()->select() and
+ * \Drupal::database()->query(), which can be called from any Drupal method or
+ * function code. In some classes, you may already have a database connection
+ * object in a member variable, or it may be passed into a class constructor
+ * via dependency injection. If that is the case, you can look at the code for
+ * \Drupal::database()->select() and the other functions to see how to get a
+ * query object from your connection variable. For example:
* @code
* $query = $connection->select('example', 'e');
* @endcode
* would be the equivalent of
* @code
- * $query = db_select('example', 'e');
+ * $query = \Drupal::database()->select('example', 'e');
* @endcode
* if you had a connection object variable $connection available to use. See
* also the @link container Services and Dependency Injection topic. @endlink
@@ -242,21 +248,18 @@ use Drupal\Core\Database\Query\Condition;
*
* The following keys are defined:
* - 'description': A string in non-markup plain text describing this table
- * and its purpose. References to other tables should be enclosed in
- * curly-brackets. For example, the node_field_revision table
- * description field might contain "Stores per-revision title and
- * body data for each {node}."
+ * and its purpose. References to other tables should be enclosed in curly
+ * brackets.
* - 'fields': An associative array ('fieldname' => specification)
* that describes the table's database columns. The specification
* is also an array. The following specification parameters are defined:
* - 'description': A string in non-markup plain text describing this field
- * and its purpose. References to other tables should be enclosed in
- * curly-brackets. For example, the node table vid field
- * description might contain "Always holds the largest (most
- * recent) {node_field_revision}.vid value for this nid."
+ * and its purpose. References to other tables should be enclosed in curly
+ * brackets. For example, the users_data table 'uid' field description
+ * might contain "The {users}.uid this record affects."
* - 'type': The generic datatype: 'char', 'varchar', 'text', 'blob', 'int',
* 'float', 'numeric', or 'serial'. Most types just map to the according
- * database engine specific datatypes. Use 'serial' for auto incrementing
+ * database engine specific data types. Use 'serial' for auto incrementing
* fields. This will expand to 'INT auto_increment' on MySQL.
* A special 'varchar_ascii' type is also available for limiting machine
* name field to US ASCII characters.
@@ -272,7 +275,7 @@ use Drupal\Core\Database\Query\Condition;
* - 'size': The data size: 'tiny', 'small', 'medium', 'normal',
* 'big'. This is a hint about the largest value the field will
* store and determines which of the database engine specific
- * datatypes will be used (e.g. on MySQL, TINYINT vs. INT vs. BIGINT).
+ * data types will be used (e.g. on MySQL, TINYINT vs. INT vs. BIGINT).
* 'normal', the default, selects the base type (e.g. on MySQL,
* INT, VARCHAR, BLOB, etc.).
* Not all sizes are available for all data types. See
@@ -316,64 +319,70 @@ use Drupal\Core\Database\Query\Condition;
* key column specifiers (see below) that form an index on the
* table.
*
- * A key column specifier is either a string naming a column or an
- * array of two elements, column name and length, specifying a prefix
- * of the named column.
+ * A key column specifier is either a string naming a column or an array of two
+ * elements, column name and length, specifying a prefix of the named column.
*
- * As an example, here is a SUBSET of the schema definition for
- * Drupal's 'node' table. It show four fields (nid, vid, type, and
- * title), the primary key on field 'nid', a unique key named 'vid' on
- * field 'vid', and two indexes, one named 'nid' on field 'nid' and
- * one named 'node_title_type' on the field 'title' and the first four
- * bytes of the field 'type':
+ * As an example, this is the schema definition for the 'users_data' table. It
+ * shows five fields ('uid', 'module', 'name', 'value', and 'serialized'), the
+ * primary key (on the 'uid', 'module', and 'name' fields), and two indexes (the
+ * 'module' index on the 'module' field and the 'name' index on the 'name'
+ * field).
*
* @code
- * $schema['node'] = array(
- * 'description' => 'The base table for nodes.',
- * 'fields' => array(
- * 'nid' => array('type' => 'serial', 'unsigned' => TRUE, 'not null' => TRUE),
- * 'vid' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE,'default' => 0),
- * 'type' => array('type' => 'varchar','length' => 32,'not null' => TRUE, 'default' => ''),
- * 'language' => array('type' => 'varchar','length' => 12,'not null' => TRUE,'default' => ''),
- * 'title' => array('type' => 'varchar','length' => 255,'not null' => TRUE, 'default' => ''),
- * 'uid' => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
- * 'status' => array('type' => 'int', 'not null' => TRUE, 'default' => 1),
- * 'created' => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
- * 'changed' => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
- * 'comment' => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
- * 'promote' => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
- * 'moderate' => array('type' => 'int', 'not null' => TRUE,'default' => 0),
- * 'sticky' => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
- * 'translate' => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
- * ),
- * 'indexes' => array(
- * 'node_changed' => array('changed'),
- * 'node_created' => array('created'),
- * 'node_moderate' => array('moderate'),
- * 'node_frontpage' => array('promote', 'status', 'sticky', 'created'),
- * 'node_status_type' => array('status', 'type', 'nid'),
- * 'node_title_type' => array('title', array('type', 4)),
- * 'node_type' => array(array('type', 4)),
- * 'uid' => array('uid'),
- * 'translate' => array('translate'),
- * ),
- * 'unique keys' => array(
- * 'vid' => array('vid'),
- * ),
+ * $schema['users_data'] = [
+ * 'description' => 'Stores module data as key/value pairs per user.',
+ * 'fields' => [
+ * 'uid' => [
+ * 'description' => 'The {users}.uid this record affects.',
+ * 'type' => 'int',
+ * 'unsigned' => TRUE,
+ * 'not null' => TRUE,
+ * 'default' => 0,
+ * ],
+ * 'module' => [
+ * 'description' => 'The name of the module declaring the variable.',
+ * 'type' => 'varchar_ascii',
+ * 'length' => DRUPAL_EXTENSION_NAME_MAX_LENGTH,
+ * 'not null' => TRUE,
+ * 'default' => '',
+ * ],
+ * 'name' => [
+ * 'description' => 'The identifier of the data.',
+ * 'type' => 'varchar_ascii',
+ * 'length' => 128,
+ * 'not null' => TRUE,
+ * 'default' => '',
+ * ],
+ * 'value' => [
+ * 'description' => 'The value.',
+ * 'type' => 'blob',
+ * 'not null' => FALSE,
+ * 'size' => 'big',
+ * ],
+ * 'serialized' => [
+ * 'description' => 'Whether value is serialized.',
+ * 'type' => 'int',
+ * 'size' => 'tiny',
+ * 'unsigned' => TRUE,
+ * 'default' => 0,
+ * ],
+ * ],
+ * 'primary key' => ['uid', 'module', 'name'],
+ * 'indexes' => [
+ * 'module' => ['module'],
+ * 'name' => ['name'],
+ * ],
* // For documentation purposes only; foreign keys are not created in the
* // database.
- * 'foreign keys' => array(
- * 'node_revision' => array(
- * 'table' => 'node_field_revision',
- * 'columns' => array('vid' => 'vid'),
- * ),
- * 'node_author' => array(
+ * 'foreign keys' => [
+ * 'data_user' => [
* 'table' => 'users',
- * 'columns' => array('uid' => 'uid'),
- * ),
- * ),
- * 'primary key' => array('nid'),
- * );
+ * 'columns' => [
+ * 'uid' => 'uid',
+ * ],
+ * ],
+ * ],
+ * ];
* @endcode
*
* @see drupal_install_schema()
@@ -484,60 +493,61 @@ function hook_query_TAG_alter(Drupal\Core\Database\Query\AlterableInterface $que
* @ingroup schemaapi
*/
function hook_schema() {
- $schema['node'] = [
- // Example (partial) specification for table "node".
- 'description' => 'The base table for nodes.',
+ $schema['users_data'] = [
+ 'description' => 'Stores module data as key/value pairs per user.',
'fields' => [
- 'nid' => [
- 'description' => 'The primary identifier for a node.',
- 'type' => 'serial',
- 'unsigned' => TRUE,
- 'not null' => TRUE,
- ],
- 'vid' => [
- 'description' => 'The current {node_field_revision}.vid version identifier.',
+ 'uid' => [
+ 'description' => 'The {users}.uid this record affects.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
],
- 'type' => [
- 'description' => 'The type of this node.',
- 'type' => 'varchar',
- 'length' => 32,
+ 'module' => [
+ 'description' => 'The name of the module declaring the variable.',
+ 'type' => 'varchar_ascii',
+ 'length' => DRUPAL_EXTENSION_NAME_MAX_LENGTH,
'not null' => TRUE,
'default' => '',
],
- 'title' => [
- 'description' => 'The node title.',
- 'type' => 'varchar',
- 'length' => 255,
+ 'name' => [
+ 'description' => 'The identifier of the data.',
+ 'type' => 'varchar_ascii',
+ 'length' => 128,
'not null' => TRUE,
'default' => '',
],
+ 'value' => [
+ 'description' => 'The value.',
+ 'type' => 'blob',
+ 'not null' => FALSE,
+ 'size' => 'big',
+ ],
+ 'serialized' => [
+ 'description' => 'Whether value is serialized.',
+ 'type' => 'int',
+ 'size' => 'tiny',
+ 'unsigned' => TRUE,
+ 'default' => 0,
+ ],
],
+ 'primary key' => ['uid', 'module', 'name'],
'indexes' => [
- 'node_changed' => ['changed'],
- 'node_created' => ['created'],
- ],
- 'unique keys' => [
- 'nid_vid' => ['nid', 'vid'],
- 'vid' => ['vid'],
+ 'module' => ['module'],
+ 'name' => ['name'],
],
// For documentation purposes only; foreign keys are not created in the
// database.
'foreign keys' => [
- 'node_revision' => [
- 'table' => 'node_field_revision',
- 'columns' => ['vid' => 'vid'],
- ],
- 'node_author' => [
+ 'data_user' => [
'table' => 'users',
- 'columns' => ['uid' => 'uid'],
+ 'columns' => [
+ 'uid' => 'uid',
+ ],
],
],
- 'primary key' => ['nid'],
];
+
return $schema;
}
diff --git a/core/lib/Drupal/Core/Datetime/DateHelper.php b/core/lib/Drupal/Core/Datetime/DateHelper.php
index b3a8ce7..168ef62 100644
--- a/core/lib/Drupal/Core/Datetime/DateHelper.php
+++ b/core/lib/Drupal/Core/Datetime/DateHelper.php
@@ -334,7 +334,6 @@ class DateHelper {
return !$required ? $none + $range : $range;
}
-
/**
* Constructs an array of hours.
*
diff --git a/core/lib/Drupal/Core/Datetime/DrupalDateTime.php b/core/lib/Drupal/Core/Datetime/DrupalDateTime.php
index bb7b821..f921b45 100644
--- a/core/lib/Drupal/Core/Datetime/DrupalDateTime.php
+++ b/core/lib/Drupal/Core/Datetime/DrupalDateTime.php
@@ -38,7 +38,7 @@ class DrupalDateTime extends DateTimePlus {
* timezone are ignored when the $time parameter either is a UNIX timestamp
* (e.g. @946684800) or specifies a timezone
* (e.g. 2010-01-28T15:00:00+02:00).
- * @see http://php.net/manual/en/datetime.construct.php
+ * @see http://php.net/manual/datetime.construct.php
* @param array $settings
* - validate_format: (optional) Boolean choice to validate the
* created date using the input format. The format used in
diff --git a/core/lib/Drupal/Core/Datetime/Element/Datetime.php b/core/lib/Drupal/Core/Datetime/Element/Datetime.php
index e238ff5..bfdaa40 100644
--- a/core/lib/Drupal/Core/Datetime/Element/Datetime.php
+++ b/core/lib/Drupal/Core/Datetime/Element/Datetime.php
@@ -70,8 +70,8 @@ class Datetime extends DateElementBase {
*/
public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
if ($input !== FALSE) {
- $date_input = $element['#date_date_element'] != 'none' && !empty($input['date']) ? $input['date'] : '';
- $time_input = $element['#date_time_element'] != 'none' && !empty($input['time']) ? $input['time'] : '';
+ $date_input = $element['#date_date_element'] != 'none' && !empty($input['date']) ? $input['date'] : '';
+ $time_input = $element['#date_time_element'] != 'none' && !empty($input['time']) ? $input['time'] : '';
$date_format = $element['#date_date_element'] != 'none' ? static::getHtml5DateFormat($element) : '';
$time_format = $element['#date_time_element'] != 'none' ? static::getHtml5TimeFormat($element) : '';
$timezone = !empty($element['#date_timezone']) ? $element['#date_timezone'] : NULL;
@@ -181,9 +181,13 @@ class Datetime extends DateElementBase {
* dynamic value that is that many years earlier or later than the current
* year at the time the form is displayed. Used in jQueryUI datepicker year
* range and HTML5 min/max date settings. Defaults to '1900:2050'.
- * - #date_increment: The increment to use for minutes and seconds, i.e.
- * '15' would show only :00, :15, :30 and :45. Used for HTML5 step values and
- * jQueryUI datepicker settings. Defaults to 1 to show every minute.
+ * - #date_increment: The interval (step) to use when incrementing or
+ * decrementing time, in seconds. For example, if this value is set to 30,
+ * time increases (or decreases) in steps of 30 seconds (00:00:00,
+ * 00:00:30, 00:01:00, and so on.) If this value is a multiple of 60, the
+ * "seconds"-component will not be shown in the input. Used for HTML5 step
+ * values and jQueryUI datepicker settings. Defaults to 1 to show every
+ * second.
* - #date_timezone: The local timezone to use when creating dates. Generally
* this should be left empty and it will be set correctly for the user using
* the form. Useful if the default value is empty to designate a desired
diff --git a/core/lib/Drupal/Core/DependencyInjection/Compiler/CorsCompilerPass.php b/core/lib/Drupal/Core/DependencyInjection/Compiler/CorsCompilerPass.php
index 207e094..3e0344a 100644
--- a/core/lib/Drupal/Core/DependencyInjection/Compiler/CorsCompilerPass.php
+++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/CorsCompilerPass.php
@@ -22,7 +22,7 @@ class CorsCompilerPass implements CompilerPassInterface {
$enabled = !empty($cors_config['enabled']);
}
- // Remove the CORS middleware completly in case it was not enabled.
+ // Remove the CORS middleware completely in case it was not enabled.
if (!$enabled) {
$container->removeDefinition('http_middleware.cors');
}
diff --git a/core/lib/Drupal/Core/DependencyInjection/Compiler/TwigExtensionPass.php b/core/lib/Drupal/Core/DependencyInjection/Compiler/TwigExtensionPass.php
index 73ee17b..0f2bdb0 100644
--- a/core/lib/Drupal/Core/DependencyInjection/Compiler/TwigExtensionPass.php
+++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/TwigExtensionPass.php
@@ -9,8 +9,8 @@ use Symfony\Component\DependencyInjection\ContainerBuilder;
/**
* Adds the twig_extension_hash parameter to the container.
*
- * twig_extension_hash is a hash of all extension mtimes for Twig template
- * invalidation.
+ * Parameter twig_extension_hash is a hash of all extension mtimes for Twig
+ * template invalidation.
*/
class TwigExtensionPass implements CompilerPassInterface {
diff --git a/core/lib/Drupal/Core/DependencyInjection/ContainerBuilder.php b/core/lib/Drupal/Core/DependencyInjection/ContainerBuilder.php
index e0becc7..9f51078 100644
--- a/core/lib/Drupal/Core/DependencyInjection/ContainerBuilder.php
+++ b/core/lib/Drupal/Core/DependencyInjection/ContainerBuilder.php
@@ -27,8 +27,8 @@ class ContainerBuilder extends SymfonyContainerBuilder {
* {@inheritdoc}
*/
public function __construct(ParameterBagInterface $parameterBag = NULL) {
- $this->setResourceTracking(FALSE);
parent::__construct($parameterBag);
+ $this->setResourceTracking(FALSE);
}
/**
@@ -46,9 +46,12 @@ class ContainerBuilder extends SymfonyContainerBuilder {
}
/**
- * {@inheritdoc}
+ * A 1to1 copy of parent::shareService.
+ *
+ * @todo https://www.drupal.org/project/drupal/issues/2937010 Since Symfony
+ * 3.4 this is not a 1to1 copy.
*/
- protected function shareService(Definition $definition, $service, $id)
+ protected function shareService(Definition $definition, $service, $id, array &$inlineServices)
{
if ($definition->isShared()) {
$this->services[$lowerId = strtolower($id)] = $service;
@@ -91,6 +94,32 @@ class ContainerBuilder extends SymfonyContainerBuilder {
/**
* {@inheritdoc}
*/
+ public function setAlias($alias, $id) {
+ $alias = parent::setAlias($alias, $id);
+ // As of Symfony 3.4 all aliases are private by default.
+ $alias->setPublic(TRUE);
+ return $alias;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setDefinition($id, Definition $definition) {
+ $definition = parent::setDefinition($id, $definition);
+ // As of Symfony 3.4 all definitions are private by default.
+ // \Symfony\Component\DependencyInjection\Compiler\ResolvePrivatesPassOnly
+ // removes services marked as private from the container even if they are
+ // also marked as public. Drupal requires services that are public to
+ // remain in the container and not be removed.
+ if ($definition->isPublic()) {
+ $definition->setPrivate(FALSE);
+ }
+ return $definition;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
public function setParameter($name, $value) {
if (strtolower($name) !== $name) {
throw new \InvalidArgumentException("Parameter names must be lowercase: $name");
@@ -100,8 +129,11 @@ class ContainerBuilder extends SymfonyContainerBuilder {
/**
* A 1to1 copy of parent::callMethod.
+ *
+ * @todo https://www.drupal.org/project/drupal/issues/2937010 Since Symfony
+ * 3.4 this is not a 1to1 copy.
*/
- protected function callMethod($service, $call) {
+ protected function callMethod($service, $call, array &$inlineServices = array()) {
$services = self::getServiceConditionals($call[1]);
foreach ($services as $s) {
diff --git a/core/lib/Drupal/Core/DependencyInjection/DependencySerializationTrait.php b/core/lib/Drupal/Core/DependencyInjection/DependencySerializationTrait.php
index 37e7f82..b151a3b 100644
--- a/core/lib/Drupal/Core/DependencyInjection/DependencySerializationTrait.php
+++ b/core/lib/Drupal/Core/DependencyInjection/DependencySerializationTrait.php
@@ -2,6 +2,7 @@
namespace Drupal\Core\DependencyInjection;
+use Drupal\Core\Entity\EntityStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
@@ -17,6 +18,13 @@ trait DependencySerializationTrait {
protected $_serviceIds = [];
/**
+ * An array of entity type IDs keyed by the property name of their storages.
+ *
+ * @var array
+ */
+ protected $_entityStorages = [];
+
+ /**
* {@inheritdoc}
*/
public function __sleep() {
@@ -35,6 +43,17 @@ trait DependencySerializationTrait {
$this->_serviceIds[$key] = 'service_container';
unset($vars[$key]);
}
+ elseif ($value instanceof EntityStorageInterface) {
+ // If a class member is an entity storage, only store the entity type ID
+ // the storage is for so it can be used to get a fresh object on
+ // unserialization. By doing this we prevent possible memory leaks when
+ // the storage is serialized when it contains a static cache of entity
+ // objects and additionally we ensure that we'll not have multiple
+ // storage objects for the same entity type and therefore prevent
+ // returning different references for the same entity.
+ $this->_entityStorages[$key] = $value->getEntityTypeId();
+ unset($vars[$key]);
+ }
}
return array_keys($vars);
@@ -61,6 +80,19 @@ trait DependencySerializationTrait {
$this->$key = $container->get($service_id);
}
$this->_serviceIds = [];
+
+ // In rare cases, when test data is serialized in the parent process, there
+ // is a service container but it doesn't contain all expected services. To
+ // avoid fatal errors during the wrap-up of failing tests, we check for this
+ // case, too.
+ if ($this->_entityStorages && (!$phpunit_bootstrap || $container->has('entity_type.manager'))) {
+ /** @var \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager */
+ $entity_type_manager = $container->get('entity_type.manager');
+ foreach ($this->_entityStorages as $key => $entity_type_id) {
+ $this->$key = $entity_type_manager->getStorage($entity_type_id);
+ }
+ }
+ $this->_entityStorages = [];
}
}
diff --git a/core/lib/Drupal/Core/DependencyInjection/YamlFileLoader.php b/core/lib/Drupal/Core/DependencyInjection/YamlFileLoader.php
index d999450..e6741fc 100644
--- a/core/lib/Drupal/Core/DependencyInjection/YamlFileLoader.php
+++ b/core/lib/Drupal/Core/DependencyInjection/YamlFileLoader.php
@@ -8,7 +8,7 @@ use Drupal\Core\Serialization\Yaml;
use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Definition;
-use Symfony\Component\DependencyInjection\DefinitionDecorator;
+use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
@@ -155,7 +155,7 @@ class YamlFileLoader
}
if (isset($service['parent'])) {
- $definition = new DefinitionDecorator($service['parent']);
+ $definition = new ChildDefinition($service['parent']);
} else {
$definition = new Definition();
}
diff --git a/core/lib/Drupal/Core/Diff/DiffFormatter.php b/core/lib/Drupal/Core/Diff/DiffFormatter.php
index 1b3604d..52f5550 100644
--- a/core/lib/Drupal/Core/Diff/DiffFormatter.php
+++ b/core/lib/Drupal/Core/Diff/DiffFormatter.php
@@ -57,7 +57,7 @@ class DiffFormatter extends DiffFormatterBase {
[
'data' => $ybeg + $this->line_stats['offset']['y'],
'colspan' => 2,
- ]
+ ],
];
}
@@ -94,7 +94,7 @@ class DiffFormatter extends DiffFormatterBase {
[
'data' => ['#markup' => $line],
'class' => 'diff-context diff-addedline',
- ]
+ ],
];
}
@@ -116,7 +116,7 @@ class DiffFormatter extends DiffFormatterBase {
[
'data' => ['#markup' => $line],
'class' => 'diff-context diff-deletedline',
- ]
+ ],
];
}
@@ -135,7 +135,7 @@ class DiffFormatter extends DiffFormatterBase {
[
'data' => ['#markup' => $line],
'class' => 'diff-context',
- ]
+ ],
];
}
diff --git a/core/lib/Drupal/Core/DrupalKernel.php b/core/lib/Drupal/Core/DrupalKernel.php
index 217d4dc..a099327 100644
--- a/core/lib/Drupal/Core/DrupalKernel.php
+++ b/core/lib/Drupal/Core/DrupalKernel.php
@@ -5,7 +5,6 @@ namespace Drupal\Core;
use Composer\Autoload\ClassLoader;
use Drupal\Component\Assertion\Handle;
use Drupal\Component\FileCache\FileCacheFactory;
-use Drupal\Component\Utility\Unicode;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Cache\DatabaseBackend;
use Drupal\Core\Config\BootstrapConfigStorageFactory;
@@ -20,6 +19,7 @@ use Drupal\Core\File\MimeType\MimeTypeGuesser;
use Drupal\Core\Http\TrustedHostsRequestFactory;
use Drupal\Core\Installer\InstallerRedirectTrait;
use Drupal\Core\Language\Language;
+use Drupal\Core\Security\RequestSanitizer;
use Drupal\Core\Site\Settings;
use Drupal\Core\Test\TestDatabase;
use Symfony\Cmf\Component\Routing\RouteObjectInterface;
@@ -298,12 +298,16 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
}
/**
- * Determine the application root directory based on assumptions.
+ * Determine the application root directory based on this file's location.
*
* @return string
* The application root.
*/
protected static function guessApplicationRoot() {
+ // Determine the application root by:
+ // - Removing the namespace directories from the path.
+ // - Getting the path to the directory two levels up from the path
+ // determined in the previous step.
return dirname(dirname(substr(__DIR__, 0, -strlen(__NAMESPACE__))));
}
@@ -317,6 +321,9 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
* for bootstrap level configuration, file configuration stores, public file
* storage and site specific modules and themes.
*
+ * A file named sites.php must be present in the sites directory for
+ * multisite. If it doesn't exist, then 'sites/default' will be used.
+ *
* Finds a matching site directory file by stripping the website's hostname
* from left to right and pathname from right to left. By default, the
* directory must contain a 'settings.php' file for it to match. If the
@@ -327,9 +334,8 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
* default.settings.php for examples on how the URL is converted to a
* directory.
*
- * If a file named sites.php is present in the sites directory, it will be
- * loaded prior to scanning for directories. That file can define aliases in
- * an associative array named $sites. The array is written in the format
+ * The sites.php file in the sites directory can define aliases in an
+ * associative array named $sites. The array is written in the format
* '<port>.<domain>.<path>' => 'directory'. As an example, to create a
* directory alias for https://www.drupal.org:8080/mysite/test whose
* configuration file is in sites/example.com, the array should be defined as:
@@ -542,6 +548,12 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
* {@inheritdoc}
*/
public function preHandle(Request $request) {
+ // Sanitize the request.
+ $request = RequestSanitizer::sanitize(
+ $request,
+ (array) Settings::get(RequestSanitizer::SANITIZE_WHITELIST, []),
+ (bool) Settings::get(RequestSanitizer::SANITIZE_LOG, FALSE)
+ );
$this->loadLegacyIncludes();
@@ -676,13 +688,13 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
*
* @param \Exception $e
* An exception
- * @param Request $request
+ * @param \Symfony\Component\HttpFoundation\Request $request
* A Request instance
* @param int $type
* The type of the request (one of HttpKernelInterface::MASTER_REQUEST or
* HttpKernelInterface::SUB_REQUEST)
*
- * @return Response
+ * @return \Symfony\Component\HttpFoundation\Response
* A Response instance
*
* @throws \Exception
@@ -974,23 +986,26 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
// sites/default/default.settings.php contains more runtime settings.
// The .htaccess file contains settings that cannot be changed at runtime.
- // Use session cookies, not transparent sessions that puts the session id in
- // the query string.
- ini_set('session.use_cookies', '1');
- ini_set('session.use_only_cookies', '1');
- ini_set('session.use_trans_sid', '0');
- // Don't send HTTP headers using PHP's session handler.
- // Send an empty string to disable the cache limiter.
- ini_set('session.cache_limiter', '');
- // Use httponly session cookies.
- ini_set('session.cookie_httponly', '1');
+ if (PHP_SAPI !== 'cli') {
+ // Use session cookies, not transparent sessions that puts the session id
+ // in the query string.
+ ini_set('session.use_cookies', '1');
+ ini_set('session.use_only_cookies', '1');
+ ini_set('session.use_trans_sid', '0');
+ // Don't send HTTP headers using PHP's session handler.
+ // Send an empty string to disable the cache limiter.
+ ini_set('session.cache_limiter', '');
+ // Use httponly session cookies.
+ ini_set('session.cookie_httponly', '1');
+ }
// Set sane locale settings, to ensure consistent string, dates, times and
// numbers handling.
setlocale(LC_ALL, 'C');
- // Detect string handling method.
- Unicode::check();
+ // Set appropriate configuration for multi-byte strings.
+ mb_internal_encoding('utf-8');
+ mb_language('uni');
// Indicate that code is operating in a test child site.
if (!defined('DRUPAL_TEST_IN_CHILD_SITE')) {
@@ -1080,7 +1095,7 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
// misses.
$old_loader = $this->classLoader;
$this->classLoader = $loader;
- // Our class loaders are preprended to ensure they come first like the
+ // Our class loaders are prepended to ensure they come first like the
// class loader they are replacing.
$old_loader->register(TRUE);
$loader->register(TRUE);
@@ -1187,10 +1202,10 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
/**
* Attach synthetic values on to kernel.
*
- * @param ContainerInterface $container
+ * @param \Symfony\Component\DependencyInjection\ContainerInterface $container
* Container object
*
- * @return ContainerInterface
+ * @return \Symfony\Component\DependencyInjection\ContainerInterface
*/
protected function attachSynthetic(ContainerInterface $container) {
$persist = [];
@@ -1213,7 +1228,7 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
/**
* Compiles a new service container.
*
- * @return ContainerBuilder The compiled service container
+ * @return \Drupal\Core\DependencyInjection\ContainerBuilder The compiled service container
*/
protected function compileContainer() {
// We are forcing a container build so it is reasonable to assume that the
@@ -1334,7 +1349,7 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
/**
* Gets a new ContainerBuilder instance used to build the service container.
*
- * @return ContainerBuilder
+ * @return \Drupal\Core\DependencyInjection\ContainerBuilder
*/
protected function getContainerBuilder() {
return new ContainerBuilder(new ParameterBag($this->getKernelParameters()));
@@ -1593,13 +1608,14 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
*/
protected function getInstallProfile() {
$config = $this->getConfigStorage()->read('core.extension');
- if (!empty($config['profile'])) {
+ if (isset($config['profile'])) {
$install_profile = $config['profile'];
}
// @todo https://www.drupal.org/node/2831065 remove the BC layer.
else {
// If system_update_8300() has not yet run fallback to using settings.
- $install_profile = Settings::get('install_profile');
+ $settings = Settings::getAll();
+ $install_profile = isset($settings['install_profile']) ? $settings['install_profile'] : NULL;
}
// Normalize an empty string to a NULL value.
diff --git a/core/lib/Drupal/Core/Entity/Annotation/EntityReferenceSelection.php b/core/lib/Drupal/Core/Entity/Annotation/EntityReferenceSelection.php
index 96bc6b1..88f86db 100644
--- a/core/lib/Drupal/Core/Entity/Annotation/EntityReferenceSelection.php
+++ b/core/lib/Drupal/Core/Entity/Annotation/EntityReferenceSelection.php
@@ -63,7 +63,7 @@ class EntityReferenceSelection extends Plugin {
public $entity_types = [];
/**
- * The weight of the plugin in it's group.
+ * The weight of the plugin in its group.
*
* @var int
*/
diff --git a/core/lib/Drupal/Core/Entity/Annotation/EntityType.php b/core/lib/Drupal/Core/Entity/Annotation/EntityType.php
index 99335ae..9d14bed 100644
--- a/core/lib/Drupal/Core/Entity/Annotation/EntityType.php
+++ b/core/lib/Drupal/Core/Entity/Annotation/EntityType.php
@@ -33,6 +33,8 @@ class EntityType extends Plugin {
/**
* The group machine name.
+ *
+ * @var string
*/
public $group = 'default';
diff --git a/core/lib/Drupal/Core/Entity/BundleEntityFormBase.php b/core/lib/Drupal/Core/Entity/BundleEntityFormBase.php
index f51dd36..aa51cbc 100644
--- a/core/lib/Drupal/Core/Entity/BundleEntityFormBase.php
+++ b/core/lib/Drupal/Core/Entity/BundleEntityFormBase.php
@@ -22,7 +22,7 @@ class BundleEntityFormBase extends EntityForm {
protected function protectBundleIdElement(array $form) {
$entity = $this->getEntity();
$id_key = $entity->getEntityType()->getKey('id');
- assert('isset($form[$id_key])');
+ assert(isset($form[$id_key]));
$element = &$form[$id_key];
// Make sure the element is not accidentally re-enabled if it has already
diff --git a/core/lib/Drupal/Core/Entity/ContentEntityBase.php b/core/lib/Drupal/Core/Entity/ContentEntityBase.php
index 2c62589..2bf1372 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityBase.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php
@@ -2,10 +2,9 @@
namespace Drupal\Core\Entity;
-use Drupal\Component\Utility\SafeMarkup;
+use Drupal\Component\Render\FormattableMarkup;
use Drupal\Core\Entity\Plugin\DataType\EntityReference;
use Drupal\Core\Field\BaseFieldDefinition;
-use Drupal\Core\Field\ChangedFieldItemList;
use Drupal\Core\Language\Language;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Session\AccountInterface;
@@ -20,6 +19,10 @@ use Drupal\Core\TypedData\TypedDataInterface;
*/
abstract class ContentEntityBase extends Entity implements \IteratorAggregate, ContentEntityInterface, TranslationStatusInterface {
+ use EntityChangesDetectionTrait {
+ getFieldsToSkipFromTranslationChangesCheck as traitGetFieldsToSkipFromTranslationChangesCheck;
+ }
+
/**
* The plain data values of the contained fields.
*
@@ -157,6 +160,29 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
protected $loadedRevisionId;
/**
+ * The revision translation affected entity key.
+ *
+ * @var string
+ */
+ protected $revisionTranslationAffectedKey;
+
+ /**
+ * Whether the revision translation affected flag has been enforced.
+ *
+ * An array, keyed by the translation language code.
+ *
+ * @var bool[]
+ */
+ protected $enforceRevisionTranslationAffected = [];
+
+ /**
+ * Local cache for fields to skip from the checking for translation changes.
+ *
+ * @var array
+ */
+ protected static $fieldsToSkipFromTranslationChangesCheck = [];
+
+ /**
* {@inheritdoc}
*/
public function __construct(array $values, $entity_type, $bundle = FALSE, $translations = []) {
@@ -164,6 +190,7 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
$this->entityKeys['bundle'] = $bundle ? $bundle : $this->entityTypeId;
$this->langcodeKey = $this->getEntityType()->getKey('langcode');
$this->defaultLangcodeKey = $this->getEntityType()->getKey('default_langcode');
+ $this->revisionTranslationAffectedKey = $this->getEntityType()->getKey('revision_translation_affected');
foreach ($values as $key => $value) {
// If the key matches an existing property set the value to the property
@@ -269,15 +296,11 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
// When saving a new revision, set any existing revision ID to NULL so as
// to ensure that a new revision will actually be created.
$this->set($this->getEntityType()->getKey('revision'), NULL);
-
- // Make sure that the flag tracking which translations are affected by the
- // current revision is reset.
- foreach ($this->translations as $langcode => $data) {
- // But skip removed translations.
- if ($this->hasTranslation($langcode)) {
- $this->getTranslation($langcode)->setRevisionTranslationAffected(NULL);
- }
- }
+ }
+ elseif (!$value && $this->newRevision) {
+ // If ::setNewRevision(FALSE) is called after ::setNewRevision(TRUE) we
+ // have to restore the loaded revision ID.
+ $this->set($this->getEntityType()->getKey('revision'), $this->getLoadedRevisionId());
}
$this->newRevision = $value;
@@ -321,18 +344,51 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
/**
* {@inheritdoc}
*/
+ public function wasDefaultRevision() {
+ /** @var \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type */
+ $entity_type = $this->getEntityType();
+ if (!$entity_type->isRevisionable()) {
+ return TRUE;
+ }
+
+ $revision_default_key = $entity_type->getRevisionMetadataKey('revision_default');
+ $value = $this->isNew() || $this->get($revision_default_key)->value;
+ return $value;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isLatestRevision() {
+ /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
+ $storage = $this->entityTypeManager()->getStorage($this->getEntityTypeId());
+
+ return $this->getLoadedRevisionId() == $storage->getLatestRevisionId($this->id());
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isLatestTranslationAffectedRevision() {
+ /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
+ $storage = $this->entityTypeManager()->getStorage($this->getEntityTypeId());
+
+ return $this->getLoadedRevisionId() == $storage->getLatestTranslationAffectedRevisionId($this->id(), $this->language()->getId());
+ }
+
+ /**
+ * {@inheritdoc}
+ */
public function isRevisionTranslationAffected() {
- $field_name = $this->getEntityType()->getKey('revision_translation_affected');
- return $this->hasField($field_name) ? $this->get($field_name)->value : TRUE;
+ return $this->hasField($this->revisionTranslationAffectedKey) ? $this->get($this->revisionTranslationAffectedKey)->value : TRUE;
}
/**
* {@inheritdoc}
*/
public function setRevisionTranslationAffected($affected) {
- $field_name = $this->getEntityType()->getKey('revision_translation_affected');
- if ($this->hasField($field_name)) {
- $this->set($field_name, $affected);
+ if ($this->hasField($this->revisionTranslationAffectedKey)) {
+ $this->set($this->revisionTranslationAffectedKey, $affected);
}
return $this;
}
@@ -340,6 +396,21 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
/**
* {@inheritdoc}
*/
+ public function isRevisionTranslationAffectedEnforced() {
+ return !empty($this->enforceRevisionTranslationAffected[$this->activeLangcode]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setRevisionTranslationAffectedEnforced($enforced) {
+ $this->enforceRevisionTranslationAffected[$this->activeLangcode] = $enforced;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
public function isDefaultTranslation() {
return $this->activeLangcode === LanguageInterface::LANGCODE_DEFAULT;
}
@@ -355,8 +426,8 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
* {@inheritdoc}
*/
public function isTranslatable() {
- // Check that the bundle is translatable, the entity has a language defined
- // and if we have more than one language on the site.
+ // Check the bundle is translatable, the entity has a language defined, and
+ // the site has more than one language.
$bundles = $this->entityManager()->getBundleInfo($this->entityTypeId);
return !empty($bundles[$this->bundle()]['translatable']) && !$this->getUntranslated()->language()->isLocked() && $this->languageManager()->isMultilingual();
}
@@ -401,6 +472,12 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
}
}
$this->translations = array_diff_key($this->translations, $removed);
+
+ // Reset the new revision flag.
+ $this->newRevision = FALSE;
+
+ // Reset the enforcement of the revision translation affected flag.
+ $this->enforceRevisionTranslationAffected = [];
}
/**
@@ -701,11 +778,11 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
* {@inheritdoc}
*/
public function onChange($name) {
- // Check if the changed name is the value of an entity key and if the value
- // of that is currently cached, if so, reset it. Exclude the bundle from
- // that check, as it ready only and must not change, unsetting it could
+ // Check if the changed name is the value of any entity keys and if any of
+ // those values are currently cached, if so, reset it. Exclude the bundle
+ // from that check, as it ready only and must not change, unsetting it could
// lead to recursions.
- if ($key = array_search($name, $this->getEntityType()->getKeys())) {
+ foreach (array_keys($this->getEntityType()->getKeys(), $name, TRUE) as $key) {
if ($key != 'bundle') {
if (isset($this->entityKeys[$key])) {
unset($this->entityKeys[$key]);
@@ -713,6 +790,12 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
elseif (isset($this->translatableEntityKeys[$key][$this->activeLangcode])) {
unset($this->translatableEntityKeys[$key][$this->activeLangcode]);
}
+ // If the revision identifier field is being populated with the original
+ // value, we need to make sure the "new revision" flag is reset
+ // accordingly.
+ if ($key === 'revision' && $this->getRevisionId() == $this->getLoadedRevisionId() && !$this->isNew()) {
+ $this->newRevision = FALSE;
+ }
}
}
@@ -722,7 +805,7 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
// Update the default internal language cache.
$this->setDefaultLangcode();
if (isset($this->translations[$this->defaultLangcode])) {
- $message = SafeMarkup::format('A translation already exists for the specified language (@langcode).', ['@langcode' => $this->defaultLangcode]);
+ $message = new FormattableMarkup('A translation already exists for the specified language (@langcode).', ['@langcode' => $this->defaultLangcode]);
throw new \InvalidArgumentException($message);
}
$this->updateFieldLangcodes($this->defaultLangcode);
@@ -733,7 +816,7 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
$items = $this->get($this->langcodeKey);
if ($items->value != $this->activeLangcode) {
$items->setValue($this->activeLangcode, FALSE);
- $message = SafeMarkup::format('The translation language cannot be changed (@langcode).', ['@langcode' => $this->activeLangcode]);
+ $message = new FormattableMarkup('The translation language cannot be changed (@langcode).', ['@langcode' => $this->activeLangcode]);
throw new \LogicException($message);
}
}
@@ -744,10 +827,16 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
// read-only. See https://www.drupal.org/node/2443991.
if (isset($this->values[$this->defaultLangcodeKey]) && $this->get($this->defaultLangcodeKey)->value != $this->isDefaultTranslation()) {
$this->get($this->defaultLangcodeKey)->setValue($this->isDefaultTranslation(), FALSE);
- $message = SafeMarkup::format('The default translation flag cannot be changed (@langcode).', ['@langcode' => $this->activeLangcode]);
+ $message = new FormattableMarkup('The default translation flag cannot be changed (@langcode).', ['@langcode' => $this->activeLangcode]);
throw new \LogicException($message);
}
break;
+
+ case $this->revisionTranslationAffectedKey:
+ // If the revision translation affected flag is being set then enforce
+ // its value.
+ $this->setRevisionTranslationAffectedEnforced(TRUE);
+ break;
}
}
@@ -763,8 +852,8 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
// Populate entity translation object cache so it will be available for all
// translation objects.
- if ($langcode == $this->activeLangcode) {
- $this->translations[$langcode]['entity'] = $this;
+ if (!isset($this->translations[$this->activeLangcode]['entity'])) {
+ $this->translations[$this->activeLangcode]['entity'] = $this;
}
// If we already have a translation object for the specified language we can
@@ -831,6 +920,7 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
$translation->typedData = NULL;
$translation->loadedRevisionId = &$this->loadedRevisionId;
$translation->isDefaultRevision = &$this->isDefaultRevision;
+ $translation->enforceRevisionTranslationAffected = &$this->enforceRevisionTranslationAffected;
return $translation;
}
@@ -1043,7 +1133,9 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
$duplicate = clone $this;
$entity_type = $this->getEntityType();
- $duplicate->{$entity_type->getKey('id')}->value = NULL;
+ if ($entity_type->hasKey('id')) {
+ $duplicate->{$entity_type->getKey('id')}->value = NULL;
+ }
$duplicate->enforceIsNew();
// Check if the entity type supports UUIDs and generate a new one if so.
@@ -1098,7 +1190,8 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
// Ensure that the following properties are actually cloned by
// overwriting the original references with ones pointing to copies of
// them: enforceIsNew, newRevision, loadedRevisionId, fields, entityKeys,
- // translatableEntityKeys, values and isDefaultRevision.
+ // translatableEntityKeys, values, isDefaultRevision and
+ // enforceRevisionTranslationAffected.
$enforce_is_new = $this->enforceIsNew;
$this->enforceIsNew = &$enforce_is_new;
@@ -1123,6 +1216,9 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
$default_revision = $this->isDefaultRevision;
$this->isDefaultRevision = &$default_revision;
+ $is_revision_translation_affected_enforced = $this->enforceRevisionTranslationAffected;
+ $this->enforceRevisionTranslationAffected = &$is_revision_translation_affected_enforced;
+
foreach ($this->fields as $name => $fields_by_langcode) {
$this->fields[$name] = [];
// Untranslatable fields may have multiple references for the same field
@@ -1287,17 +1383,11 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
* An array of field names.
*/
protected function getFieldsToSkipFromTranslationChangesCheck() {
- /** @var \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type */
- $entity_type = $this->getEntityType();
- // A list of known revision metadata fields which should be skipped from
- // the comparision.
- $fields = [
- $entity_type->getKey('revision'),
- 'revision_translation_affected',
- ];
- $fields = array_merge($fields, array_values($entity_type->getRevisionMetadataKeys()));
-
- return $fields;
+ $bundle = $this->bundle();
+ if (!isset(static::$fieldsToSkipFromTranslationChangesCheck[$this->entityTypeId][$bundle])) {
+ static::$fieldsToSkipFromTranslationChangesCheck[$this->entityTypeId][$bundle] = $this->traitGetFieldsToSkipFromTranslationChangesCheck($this);
+ }
+ return static::$fieldsToSkipFromTranslationChangesCheck[$this->entityTypeId][$bundle];
}
/**
@@ -1333,32 +1423,40 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
// possible or be meaningless.
/** @var \Drupal\Core\Entity\ContentEntityBase $translation */
$translation = $original->getTranslation($this->activeLangcode);
+ $langcode = $this->language()->getId();
// The list of fields to skip from the comparision.
$skip_fields = $this->getFieldsToSkipFromTranslationChangesCheck();
+ // We also check untranslatable fields, so that a change to those will mark
+ // all translations as affected, unless they are configured to only affect
+ // the default translation.
+ $skip_untranslatable_fields = !$this->isDefaultTranslation() && $this->isDefaultTranslationAffectedOnly();
+
foreach ($this->getFieldDefinitions() as $field_name => $definition) {
// @todo Avoid special-casing the following fields. See
// https://www.drupal.org/node/2329253.
- if (in_array($field_name, $skip_fields, TRUE)) {
+ if (in_array($field_name, $skip_fields, TRUE) || ($skip_untranslatable_fields && !$definition->isTranslatable())) {
continue;
}
- $field = $this->get($field_name);
- // When saving entities in the user interface, the changed timestamp is
- // automatically incremented by ContentEntityForm::submitForm() even if
- // nothing was actually changed. Thus, the changed time needs to be
- // ignored when determining whether there are any actual changes in the
- // entity.
- if (!($field instanceof ChangedFieldItemList) && !$definition->isComputed()) {
- $items = $field->filterEmptyItems();
- $original_items = $translation->get($field_name)->filterEmptyItems();
- if (!$items->equals($original_items)) {
- return TRUE;
- }
+ $items = $this->get($field_name)->filterEmptyItems();
+ $original_items = $translation->get($field_name)->filterEmptyItems();
+ if ($items->hasAffectingChanges($original_items, $langcode)) {
+ return TRUE;
}
}
return FALSE;
}
+ /**
+ * {@inheritdoc}
+ */
+ public function isDefaultTranslationAffectedOnly() {
+ $bundle_name = $this->bundle();
+ $bundle_info = \Drupal::service('entity_type.bundle.info')
+ ->getBundleInfo($this->getEntityTypeId());
+ return !empty($bundle_info[$bundle_name]['untranslatable_fields.default_translation_affected']);
+ }
+
}
diff --git a/core/lib/Drupal/Core/Entity/ContentEntityDeleteForm.php b/core/lib/Drupal/Core/Entity/ContentEntityDeleteForm.php
index 32c48d8..27dd2b7 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityDeleteForm.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityDeleteForm.php
@@ -39,7 +39,7 @@ class ContentEntityDeleteForm extends ContentEntityConfirmFormBase {
$form['deleted_translations'] = [
'#theme' => 'item_list',
'#title' => $this->t('The following @entity-type translations will be deleted:', [
- '@entity-type' => $entity->getEntityType()->getLowercaseLabel()
+ '@entity-type' => $entity->getEntityType()->getLowercaseLabel(),
]),
'#items' => $languages,
];
@@ -60,6 +60,7 @@ class ContentEntityDeleteForm extends ContentEntityConfirmFormBase {
public function submitForm(array &$form, FormStateInterface $form_state) {
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
$entity = $this->getEntity();
+ $message = $this->getDeletionMessage();
// Make sure that deleting a translation does not delete the whole entity.
if (!$entity->isDefaultTranslation()) {
@@ -73,7 +74,7 @@ class ContentEntityDeleteForm extends ContentEntityConfirmFormBase {
$form_state->setRedirectUrl($this->getRedirectUrl());
}
- drupal_set_message($this->getDeletionMessage());
+ $this->messenger()->addStatus($message);
$this->logDeletionMessage();
}
diff --git a/core/lib/Drupal/Core/Entity/ContentEntityForm.php b/core/lib/Drupal/Core/Entity/ContentEntityForm.php
index 92bbbae..75bde9f 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityForm.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityForm.php
@@ -16,13 +16,6 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
class ContentEntityForm extends EntityForm implements ContentEntityFormInterface {
/**
- * The entity manager.
- *
- * @var \Drupal\Core\Entity\EntityManagerInterface
- */
- protected $entityManager;
-
- /**
* The entity being used by this form.
*
* @var \Drupal\Core\Entity\ContentEntityInterface|\Drupal\Core\Entity\RevisionLogInterface
@@ -44,18 +37,28 @@ class ContentEntityForm extends EntityForm implements ContentEntityFormInterface
protected $time;
/**
+ * The entity repository service.
+ *
+ * @var \Drupal\Core\Entity\EntityRepositoryInterface
+ */
+ protected $entityRepository;
+
+ /**
* Constructs a ContentEntityForm object.
*
- * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
- * The entity manager.
+ * @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
+ * The entity repository service.
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
* The entity type bundle service.
* @param \Drupal\Component\Datetime\TimeInterface $time
* The time service.
*/
- public function __construct(EntityManagerInterface $entity_manager, EntityTypeBundleInfoInterface $entity_type_bundle_info = NULL, TimeInterface $time = NULL) {
- $this->entityManager = $entity_manager;
-
+ public function __construct(EntityRepositoryInterface $entity_repository, EntityTypeBundleInfoInterface $entity_type_bundle_info = NULL, TimeInterface $time = NULL) {
+ if ($entity_repository instanceof EntityManagerInterface) {
+ @trigger_error('Passing the entity.manager service to ContentEntityForm::__construct() is deprecated in Drupal 8.6.0 and will be removed before Drupal 9.0.0. Pass the entity.repository service instead. See https://www.drupal.org/node/2549139.', E_USER_DEPRECATED);
+ $this->entityManager = $entity_repository;
+ }
+ $this->entityRepository = $entity_repository;
$this->entityTypeBundleInfo = $entity_type_bundle_info ?: \Drupal::service('entity_type.bundle.info');
$this->time = $time ?: \Drupal::service('datetime.time');
}
@@ -65,7 +68,7 @@ class ContentEntityForm extends EntityForm implements ContentEntityFormInterface
*/
public static function create(ContainerInterface $container) {
return new static(
- $container->get('entity.manager'),
+ $container->get('entity.repository'),
$container->get('entity_type.bundle.info'),
$container->get('datetime.time')
);
@@ -131,7 +134,7 @@ class ContentEntityForm extends EntityForm implements ContentEntityFormInterface
'#type' => 'container',
'#weight' => 99,
'#attributes' => [
- 'class' => ['entity-content-form-footer']
+ 'class' => ['entity-content-form-footer'],
],
'#optional' => TRUE,
];
@@ -307,7 +310,7 @@ class ContentEntityForm extends EntityForm implements ContentEntityFormInterface
// Imply a 'view' operation to ensure users edit entities in the same
// language they are displayed. This allows to keep contextual editing
// working also for multilingual entities.
- $form_state->set('langcode', $this->entityManager->getTranslationFromContext($this->entity)->language()->getId());
+ $form_state->set('langcode', $this->entityRepository->getTranslationFromContext($this->entity)->language()->getId());
}
}
diff --git a/core/lib/Drupal/Core/Entity/ContentEntityInterface.php b/core/lib/Drupal/Core/Entity/ContentEntityInterface.php
index d045152..56cf139 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityInterface.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityInterface.php
@@ -2,73 +2,25 @@
namespace Drupal\Core\Entity;
-use Drupal\Core\TypedData\TranslatableInterface;
-
/**
* Defines a common interface for all content entity objects.
*
- * Content entities use fields for all their entity properties and are
- * translatable and revisionable, while translations and revisions can be
- * enabled per entity type. It's best practice to always implement
- * ContentEntityInterface for content-like entities that should be stored in
- * some database, and enable/disable revisions and translations as desired.
+ * Content entities use fields for all their entity properties and can be
+ * translatable and revisionable. Translations and revisions can be
+ * enabled per entity type through annotation and using entity type hooks.
+ *
+ * It's best practice to always implement ContentEntityInterface for
+ * content-like entities that should be stored in some database, and
+ * enable/disable revisions and translations as desired.
*
* When implementing this interface which extends Traversable, make sure to list
* IteratorAggregate or Iterator before this interface in the implements clause.
*
* @see \Drupal\Core\Entity\ContentEntityBase
+ * @see \Drupal\Core\Entity\EntityTypeInterface
*
* @ingroup entity_api
*/
-interface ContentEntityInterface extends \Traversable, FieldableEntityInterface, RevisionableInterface, TranslatableInterface {
-
- /**
- * Determines if the current translation of the entity has unsaved changes.
- *
- * @return bool
- * TRUE if the current translation of the entity has changes.
- */
- public function hasTranslationChanges();
-
- /**
- * Marks the current revision translation as affected.
- *
- * @param bool|null $affected
- * The flag value. A NULL value can be specified to reset the current value
- * and make sure a new value will be computed by the system.
- *
- * @return $this
- */
- public function setRevisionTranslationAffected($affected);
-
- /**
- * Checks whether the current translation is affected by the current revision.
- *
- * @return bool
- * TRUE if the entity object is affected by the current revision, FALSE
- * otherwise.
- */
- public function isRevisionTranslationAffected();
-
- /**
- * Gets the loaded Revision ID of the entity.
- *
- * @return int
- * The loaded Revision identifier of the entity, or NULL if the entity
- * does not have a revision identifier.
- */
- public function getLoadedRevisionId();
-
- /**
- * Updates the loaded Revision ID with the revision ID.
- *
- * This method should not be used, it could unintentionally cause the original
- * revision ID property value to be lost.
- *
- * @internal
- *
- * @return $this
- */
- public function updateLoadedRevisionId();
+interface ContentEntityInterface extends \Traversable, FieldableEntityInterface, TranslatableRevisionableInterface {
}
diff --git a/core/lib/Drupal/Core/Entity/ContentEntityNullStorage.php b/core/lib/Drupal/Core/Entity/ContentEntityNullStorage.php
index 3dc00c9..94efc11 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityNullStorage.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityNullStorage.php
@@ -41,6 +41,13 @@ class ContentEntityNullStorage extends ContentEntityStorageBase {
/**
* {@inheritdoc}
*/
+ public function loadMultipleRevisions(array $revision_ids) {
+ return [];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
public function deleteRevision($revision_id) {
}
diff --git a/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php
index 874a341..d38244b 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php
@@ -4,8 +4,11 @@ namespace Drupal\Core\Entity;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Cache\MemoryCache\MemoryCacheInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\Core\Language\LanguageInterface;
+use Drupal\Core\TypedData\TranslationStatusInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
@@ -35,6 +38,13 @@ abstract class ContentEntityStorageBase extends EntityStorageBase implements Con
protected $cacheBackend;
/**
+ * Stores the latest revision IDs for entities.
+ *
+ * @var array
+ */
+ protected $latestRevisionIds = [];
+
+ /**
* Constructs a ContentEntityStorageBase object.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
@@ -43,9 +53,11 @@ abstract class ContentEntityStorageBase extends EntityStorageBase implements Con
* The entity manager.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache
* The cache backend to be used.
+ * @param \Drupal\Core\Cache\MemoryCache\MemoryCacheInterface|null $memory_cache
+ * The memory cache backend.
*/
- public function __construct(EntityTypeInterface $entity_type, EntityManagerInterface $entity_manager, CacheBackendInterface $cache) {
- parent::__construct($entity_type);
+ public function __construct(EntityTypeInterface $entity_type, EntityManagerInterface $entity_manager, CacheBackendInterface $cache, MemoryCacheInterface $memory_cache = NULL) {
+ parent::__construct($entity_type, $memory_cache);
$this->bundleKey = $this->entityType->getKey('bundle');
$this->entityManager = $entity_manager;
$this->cacheBackend = $cache;
@@ -58,7 +70,8 @@ abstract class ContentEntityStorageBase extends EntityStorageBase implements Con
return new static(
$entity_type,
$container->get('entity.manager'),
- $container->get('cache.entity')
+ $container->get('cache.entity'),
+ $container->get('entity.memory_cache')
);
}
@@ -149,6 +162,69 @@ abstract class ContentEntityStorageBase extends EntityStorageBase implements Con
}
/**
+ * Checks whether any entity revision is translated.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface|\Drupal\Core\Entity\TranslatableInterface $entity
+ * The entity object to be checked.
+ *
+ * @return bool
+ * TRUE if the entity has at least one translation in any revision, FALSE
+ * otherwise.
+ *
+ * @see \Drupal\Core\TypedData\TranslatableInterface::getTranslationLanguages()
+ * @see \Drupal\Core\Entity\ContentEntityStorageBase::isAnyStoredRevisionTranslated()
+ */
+ protected function isAnyRevisionTranslated(TranslatableInterface $entity) {
+ return $entity->getTranslationLanguages(FALSE) || $this->isAnyStoredRevisionTranslated($entity);
+ }
+
+ /**
+ * Checks whether any stored entity revision is translated.
+ *
+ * A revisionable entity can have translations in a pending revision, hence
+ * the default revision may appear as not translated. This determines whether
+ * the entity has any translation in the storage and thus should be considered
+ * as multilingual.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface|\Drupal\Core\Entity\TranslatableInterface $entity
+ * The entity object to be checked.
+ *
+ * @return bool
+ * TRUE if the entity has at least one translation in any revision, FALSE
+ * otherwise.
+ *
+ * @see \Drupal\Core\TypedData\TranslatableInterface::getTranslationLanguages()
+ * @see \Drupal\Core\Entity\ContentEntityStorageBase::isAnyRevisionTranslated()
+ */
+ protected function isAnyStoredRevisionTranslated(TranslatableInterface $entity) {
+ /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
+ if ($entity->isNew()) {
+ return FALSE;
+ }
+
+ if ($entity instanceof TranslationStatusInterface) {
+ foreach ($entity->getTranslationLanguages(FALSE) as $langcode => $language) {
+ if ($entity->getTranslationStatus($langcode) === TranslationStatusInterface::TRANSLATION_EXISTING) {
+ return TRUE;
+ }
+ }
+ }
+
+ $query = $this->getQuery()
+ ->condition($this->entityType->getKey('id'), $entity->id())
+ ->condition($this->entityType->getKey('default_langcode'), 0)
+ ->accessCheck(FALSE)
+ ->range(0, 1);
+
+ if ($entity->getEntityType()->isRevisionable()) {
+ $query->allRevisions();
+ }
+
+ $result = $query->execute();
+ return !empty($result);
+ }
+
+ /**
* {@inheritdoc}
*/
public function createTranslation(ContentEntityInterface $entity, $langcode, array $values = []) {
@@ -169,6 +245,162 @@ abstract class ContentEntityStorageBase extends EntityStorageBase implements Con
/**
* {@inheritdoc}
*/
+ public function createRevision(RevisionableInterface $entity, $default = TRUE, $keep_untranslatable_fields = NULL) {
+ /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
+ $new_revision = clone $entity;
+
+ $original_keep_untranslatable_fields = $keep_untranslatable_fields;
+
+ // For translatable entities, create a merged revision of the active
+ // translation and the other translations in the default revision. This
+ // permits the creation of pending revisions that can always be saved as the
+ // new default revision without reverting changes in other languages.
+ if (!$entity->isNew() && !$entity->isDefaultRevision() && $entity->isTranslatable() && $this->isAnyRevisionTranslated($entity)) {
+ $active_langcode = $entity->language()->getId();
+ $skipped_field_names = array_flip($this->getRevisionTranslationMergeSkippedFieldNames());
+
+ // By default we copy untranslatable field values from the default
+ // revision, unless they are configured to affect only the default
+ // translation. This way we can ensure we always have only one affected
+ // translation in pending revisions. This constraint is enforced by
+ // EntityUntranslatableFieldsConstraintValidator.
+ if (!isset($keep_untranslatable_fields)) {
+ $keep_untranslatable_fields = $entity->isDefaultTranslation() && $entity->isDefaultTranslationAffectedOnly();
+ }
+
+ /** @var \Drupal\Core\Entity\ContentEntityInterface $default_revision */
+ $default_revision = $this->load($entity->id());
+ $translation_languages = $default_revision->getTranslationLanguages();
+ foreach ($translation_languages as $langcode => $language) {
+ if ($langcode == $active_langcode) {
+ continue;
+ }
+
+ $default_revision_translation = $default_revision->getTranslation($langcode);
+ $new_revision_translation = $new_revision->hasTranslation($langcode) ?
+ $new_revision->getTranslation($langcode) : $new_revision->addTranslation($langcode);
+
+ /** @var \Drupal\Core\Field\FieldItemListInterface[] $sync_items */
+ $sync_items = array_diff_key(
+ $keep_untranslatable_fields ? $default_revision_translation->getTranslatableFields() : $default_revision_translation->getFields(),
+ $skipped_field_names
+ );
+ foreach ($sync_items as $field_name => $items) {
+ $new_revision_translation->set($field_name, $items->getValue());
+ }
+
+ // Make sure the "revision_translation_affected" flag is recalculated.
+ $new_revision_translation->setRevisionTranslationAffected(NULL);
+
+ // No need to copy untranslatable field values more than once.
+ $keep_untranslatable_fields = TRUE;
+ }
+
+ // Make sure we do not inadvertently recreate removed translations.
+ foreach (array_diff_key($new_revision->getTranslationLanguages(), $translation_languages) as $langcode => $language) {
+ // Allow a new revision to be created for the active language.
+ if ($langcode !== $active_langcode) {
+ $new_revision->removeTranslation($langcode);
+ }
+ }
+
+ // The "original" property is used in various places to detect changes in
+ // field values with respect to the stored ones. If the property is not
+ // defined, the stored version is loaded explicitly. Since the merged
+ // revision generated here is not stored anywhere, we need to populate the
+ // "original" property manually, so that changes can be properly detected.
+ $new_revision->original = clone $new_revision;
+ }
+
+ // Eventually mark the new revision as such.
+ $new_revision->setNewRevision();
+ $new_revision->isDefaultRevision($default);
+
+ // Actually make sure the current translation is marked as affected, even if
+ // there are no explicit changes, to be sure this revision can be related
+ // to the correct translation.
+ $new_revision->setRevisionTranslationAffected(TRUE);
+
+ // Notify modules about the new revision.
+ $arguments = [$new_revision, $entity, $original_keep_untranslatable_fields];
+ $this->moduleHandler()->invokeAll($this->entityTypeId . '_revision_create', $arguments);
+ $this->moduleHandler()->invokeAll('entity_revision_create', $arguments);
+
+ return $new_revision;
+ }
+
+ /**
+ * Returns an array of field names to skip when merging revision translations.
+ *
+ * @return array
+ * An array of field names.
+ */
+ protected function getRevisionTranslationMergeSkippedFieldNames() {
+ /** @var \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type */
+ $entity_type = $this->getEntityType();
+
+ // A list of known revision metadata fields which should be skipped from
+ // the comparision.
+ $field_names = [
+ $entity_type->getKey('revision'),
+ $entity_type->getKey('revision_translation_affected'),
+ ];
+ $field_names = array_merge($field_names, array_values($entity_type->getRevisionMetadataKeys()));
+
+ return $field_names;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getLatestRevisionId($entity_id) {
+ if (!$this->entityType->isRevisionable()) {
+ return NULL;
+ }
+
+ if (!isset($this->latestRevisionIds[$entity_id][LanguageInterface::LANGCODE_DEFAULT])) {
+ $result = $this->getQuery()
+ ->latestRevision()
+ ->condition($this->entityType->getKey('id'), $entity_id)
+ ->accessCheck(FALSE)
+ ->execute();
+
+ $this->latestRevisionIds[$entity_id][LanguageInterface::LANGCODE_DEFAULT] = key($result);
+ }
+
+ return $this->latestRevisionIds[$entity_id][LanguageInterface::LANGCODE_DEFAULT];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getLatestTranslationAffectedRevisionId($entity_id, $langcode) {
+ if (!$this->entityType->isRevisionable()) {
+ return NULL;
+ }
+
+ if (!$this->entityType->isTranslatable()) {
+ return $this->getLatestRevisionId($entity_id);
+ }
+
+ if (!isset($this->latestRevisionIds[$entity_id][$langcode])) {
+ $result = $this->getQuery()
+ ->allRevisions()
+ ->condition($this->entityType->getKey('id'), $entity_id)
+ ->condition($this->entityType->getKey('revision_translation_affected'), 1, '=', $langcode)
+ ->range(0, 1)
+ ->sort($this->entityType->getKey('revision'), 'DESC')
+ ->accessCheck(FALSE)
+ ->execute();
+
+ $this->latestRevisionIds[$entity_id][$langcode] = key($result);
+ }
+ return $this->latestRevisionIds[$entity_id][$langcode];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition) {}
/**
@@ -244,15 +476,37 @@ abstract class ContentEntityStorageBase extends EntityStorageBase implements Con
* {@inheritdoc}
*/
public function loadRevision($revision_id) {
- $revision = $this->doLoadRevisionFieldItems($revision_id);
+ $revisions = $this->loadMultipleRevisions([$revision_id]);
+
+ return isset($revisions[$revision_id]) ? $revisions[$revision_id] : NULL;
+ }
- if ($revision) {
- $entities = [$revision->id() => $revision];
+ /**
+ * {@inheritdoc}
+ */
+ public function loadMultipleRevisions(array $revision_ids) {
+ $revisions = $this->doLoadMultipleRevisionsFieldItems($revision_ids);
+
+ // The hooks are executed with an array of entities keyed by the entity ID.
+ // As we could load multiple revisions for the same entity ID at once we
+ // have to build groups of entities where the same entity ID is present only
+ // once.
+ $entity_groups = [];
+ $entity_group_mapping = [];
+ foreach ($revisions as $revision) {
+ $entity_id = $revision->id();
+ $entity_group_key = isset($entity_group_mapping[$entity_id]) ? $entity_group_mapping[$entity_id] + 1 : 0;
+ $entity_group_mapping[$entity_id] = $entity_group_key;
+ $entity_groups[$entity_group_key][$entity_id] = $revision;
+ }
+
+ // Invoke the entity hooks for each group.
+ foreach ($entity_groups as $entities) {
$this->invokeStorageLoadHook($entities);
$this->postLoad($entities);
}
- return $revision;
+ return $revisions;
}
/**
@@ -263,10 +517,34 @@ abstract class ContentEntityStorageBase extends EntityStorageBase implements Con
*
* @return \Drupal\Core\Entity\EntityInterface|null
* The specified entity revision or NULL if not found.
+ *
+ * @deprecated in Drupal 8.5.x and will be removed before Drupal 9.0.0.
+ * \Drupal\Core\Entity\ContentEntityStorageBase::doLoadMultipleRevisionsFieldItems()
+ * should be implemented instead.
+ *
+ * @see https://www.drupal.org/node/2924915
*/
abstract protected function doLoadRevisionFieldItems($revision_id);
/**
+ * Actually loads revision field item values from the storage.
+ *
+ * @param array $revision_ids
+ * An array of revision identifiers.
+ *
+ * @return \Drupal\Core\Entity\EntityInterface[]
+ * The specified entity revisions or an empty array if none are found.
+ */
+ protected function doLoadMultipleRevisionsFieldItems($revision_ids) {
+ $revisions = [];
+ foreach ($revision_ids as $revision_id) {
+ $revisions[] = $this->doLoadRevisionFieldItems($revision_id);
+ }
+
+ return $revisions;
+ }
+
+ /**
* {@inheritdoc}
*/
protected function doSave($id, EntityInterface $entity) {
@@ -288,6 +566,15 @@ abstract class ContentEntityStorageBase extends EntityStorageBase implements Con
}
$this->populateAffectedRevisionTranslations($entity);
+
+ // Populate the "revision_default" flag. We skip this when we are resaving
+ // the revision because this is only allowed for default revisions, and
+ // these cannot be made non-default.
+ if ($this->entityType->isRevisionable() && $entity->isNewRevision()) {
+ $revision_default_key = $this->entityType->getRevisionMetadataKey('revision_default');
+ $entity->set($revision_default_key, $entity->isDefaultRevision());
+ }
+
$this->doSaveFieldItems($entity);
return $return;
@@ -587,32 +874,41 @@ abstract class ContentEntityStorageBase extends EntityStorageBase implements Con
$languages = $entity->getTranslationLanguages();
foreach ($languages as $langcode => $language) {
$translation = $entity->getTranslation($langcode);
- // Avoid populating the value if it was already manually set.
- $affected = $translation->isRevisionTranslationAffected();
- if (!isset($affected) && $translation->hasTranslationChanges()) {
- $translation->setRevisionTranslationAffected(TRUE);
+ $current_affected = $translation->isRevisionTranslationAffected();
+ if (!isset($current_affected) || ($entity->isNewRevision() && !$translation->isRevisionTranslationAffectedEnforced())) {
+ // When setting the revision translation affected flag we have to
+ // explicitly set it to not be enforced. By default it will be
+ // enforced automatically when being set, which allows us to determine
+ // if the flag has been already set outside the storage in which case
+ // we should not recompute it.
+ // @see \Drupal\Core\Entity\ContentEntityBase::setRevisionTranslationAffected().
+ $new_affected = $translation->hasTranslationChanges() ? TRUE : NULL;
+ $translation->setRevisionTranslationAffected($new_affected);
+ $translation->setRevisionTranslationAffectedEnforced(FALSE);
}
}
}
}
/**
- * Ensures integer entity IDs are valid.
+ * Ensures integer entity key values are valid.
*
* The identifier sanitization provided by this method has been introduced
* as Drupal used to rely on the database to facilitate this, which worked
* correctly with MySQL but led to errors with other DBMS such as PostgreSQL.
*
* @param array $ids
- * The entity IDs to verify.
+ * The entity key values to verify.
+ * @param string $entity_key
+ * (optional) The entity key to sanitise values for. Defaults to 'id'.
*
* @return array
- * The sanitized list of entity IDs.
+ * The sanitized list of entity key values.
*/
- protected function cleanIds(array $ids) {
+ protected function cleanIds(array $ids, $entity_key = 'id') {
$definitions = $this->entityManager->getBaseFieldDefinitions($this->entityTypeId);
- $id_definition = $definitions[$this->entityType->getKey('id')];
- if ($id_definition->getType() == 'integer') {
+ $field_name = $this->entityType->getKey($entity_key);
+ if ($field_name && $definitions[$field_name]->getType() == 'integer') {
$ids = array_filter($ids, function ($id) {
return is_numeric($id) && $id == (int) $id;
});
@@ -720,34 +1016,23 @@ abstract class ContentEntityStorageBase extends EntityStorageBase implements Con
*/
public function resetCache(array $ids = NULL) {
if ($ids) {
- $cids = [];
- foreach ($ids as $id) {
- unset($this->entities[$id]);
- $cids[] = $this->buildCacheId($id);
- }
+ parent::resetCache($ids);
if ($this->entityType->isPersistentlyCacheable()) {
+ $cids = [];
+ foreach ($ids as $id) {
+ unset($this->latestRevisionIds[$id]);
+ $cids[] = $this->buildCacheId($id);
+ }
$this->cacheBackend->deleteMultiple($cids);
}
}
else {
- $this->entities = [];
+ parent::resetCache();
if ($this->entityType->isPersistentlyCacheable()) {
Cache::invalidateTags([$this->entityTypeId . '_values']);
}
+ $this->latestRevisionIds = [];
}
}
- /**
- * Builds the cache ID for the passed in entity ID.
- *
- * @param int $id
- * Entity ID for which the cache ID should be built.
- *
- * @return string
- * Cache ID that can be passed to the cache backend.
- */
- protected function buildCacheId($id) {
- return "values:{$this->entityTypeId}:$id";
- }
-
}
diff --git a/core/lib/Drupal/Core/Entity/ContentEntityStorageInterface.php b/core/lib/Drupal/Core/Entity/ContentEntityStorageInterface.php
index eb979c7..9b35a84 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityStorageInterface.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityStorageInterface.php
@@ -5,24 +5,7 @@ namespace Drupal\Core\Entity;
/**
* A storage that supports content entity types.
*/
-interface ContentEntityStorageInterface extends EntityStorageInterface {
-
- /**
- * Constructs a new entity translation object, without permanently saving it.
- *
- * @param \Drupal\Core\Entity\ContentEntityInterface $entity
- * The entity object being translated.
- * @param string $langcode
- * The translation language code.
- * @param array $values
- * (optional) An associative array of initial field values keyed by field
- * name. If none is provided default values will be applied.
- *
- * @return \Drupal\Core\Entity\ContentEntityInterface
- * A new entity translation object.
- */
- public function createTranslation(ContentEntityInterface $entity, $langcode, array $values = []);
-
+interface ContentEntityStorageInterface extends EntityStorageInterface, TranslatableRevisionableStorageInterface {
/**
* Creates an entity with sample field values.
diff --git a/core/lib/Drupal/Core/Entity/ContentEntityType.php b/core/lib/Drupal/Core/Entity/ContentEntityType.php
index 0e26c3b..3871405 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityType.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityType.php
@@ -15,14 +15,45 @@ class ContentEntityType extends EntityType implements ContentEntityTypeInterface
protected $revision_metadata_keys = [];
/**
+ * The required revision metadata keys.
+ *
+ * This property should only be filled in the constructor. This ensures that
+ * only new instances get newly added required revision metadata keys.
+ * Unserialized objects will only retrieve the keys that they already have
+ * been cached with.
+ *
+ * @var array
+ */
+ protected $requiredRevisionMetadataKeys = [];
+
+ /**
* {@inheritdoc}
*/
public function __construct($definition) {
parent::__construct($definition);
+
$this->handlers += [
'storage' => 'Drupal\Core\Entity\Sql\SqlContentEntityStorage',
'view_builder' => 'Drupal\Core\Entity\EntityViewBuilder',
];
+
+ // Only new instances should provide the required revision metadata keys.
+ // The cached instances should return only what already has been stored
+ // under the property $revision_metadata_keys. The BC layer in
+ // ::getRevisionMetadataKeys() has to detect if the revision metadata keys
+ // have been provided by the entity type annotation, therefore we add keys
+ // to the property $requiredRevisionMetadataKeys only if those keys aren't
+ // set in the entity type annotation.
+ if (!isset($this->revision_metadata_keys['revision_default'])) {
+ $this->requiredRevisionMetadataKeys['revision_default'] = 'revision_default';
+ }
+
+ // Add the required revision metadata fields here instead in the getter
+ // method, so that they are serialized as part of the object even if the
+ // getter method doesn't get called. This allows the list to be further
+ // extended. Only new instances of the class will contain the new list,
+ // while the cached instances contain the previous version of the list.
+ $this->revision_metadata_keys += $this->requiredRevisionMetadataKeys;
}
/**
@@ -54,7 +85,7 @@ class ContentEntityType extends EntityType implements ContentEntityTypeInterface
public function getRevisionMetadataKeys($include_backwards_compatibility_field_names = TRUE) {
// Provide backwards compatibility in case the revision metadata keys are
// not defined in the entity annotation.
- if (!$this->revision_metadata_keys && $include_backwards_compatibility_field_names) {
+ if ((!$this->revision_metadata_keys || ($this->revision_metadata_keys == $this->requiredRevisionMetadataKeys)) && $include_backwards_compatibility_field_names) {
$base_fields = \Drupal::service('entity_field.manager')->getBaseFieldDefinitions($this->id());
if ((isset($base_fields['revision_uid']) && $revision_user = 'revision_uid') || (isset($base_fields['revision_user']) && $revision_user = 'revision_user')) {
@trigger_error('The revision_user revision metadata key is not set.', E_USER_DEPRECATED);
diff --git a/core/lib/Drupal/Core/Entity/Controller/EntityListController.php b/core/lib/Drupal/Core/Entity/Controller/EntityListController.php
index d8a1ea1..ee35467 100644
--- a/core/lib/Drupal/Core/Entity/Controller/EntityListController.php
+++ b/core/lib/Drupal/Core/Entity/Controller/EntityListController.php
@@ -16,7 +16,8 @@ class EntityListController extends ControllerBase {
* The entity type to render.
*
* @return array
- * A render array as expected by drupal_render().
+ * A render array as expected by
+ * \Drupal\Core\Render\RendererInterface::render().
*/
public function listing($entity_type) {
return $this->entityManager()->getListBuilder($entity_type)->render();
diff --git a/core/lib/Drupal/Core/Entity/Controller/EntityViewController.php b/core/lib/Drupal/Core/Entity/Controller/EntityViewController.php
index 983eec8..7922ef4 100644
--- a/core/lib/Drupal/Core/Entity/Controller/EntityViewController.php
+++ b/core/lib/Drupal/Core/Entity/Controller/EntityViewController.php
@@ -88,7 +88,8 @@ class EntityViewController implements ContainerInjectionInterface {
* Defaults to 'full'.
*
* @return array
- * A render array as expected by drupal_render().
+ * A render array as expected by
+ * \Drupal\Core\Render\RendererInterface::render().
*/
public function view(EntityInterface $_entity, $view_mode = 'full') {
$page = $this->entityManager
diff --git a/core/lib/Drupal/Core/Entity/DynamicallyFieldableEntityStorageInterface.php b/core/lib/Drupal/Core/Entity/DynamicallyFieldableEntityStorageInterface.php
index 04d1b1e..cf303a2 100644
--- a/core/lib/Drupal/Core/Entity/DynamicallyFieldableEntityStorageInterface.php
+++ b/core/lib/Drupal/Core/Entity/DynamicallyFieldableEntityStorageInterface.php
@@ -2,9 +2,7 @@
namespace Drupal\Core\Entity;
-use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldDefinitionListenerInterface;
-use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Field\FieldStorageDefinitionListenerInterface;
/**
@@ -18,27 +16,4 @@ use Drupal\Core\Field\FieldStorageDefinitionListenerInterface;
*/
interface DynamicallyFieldableEntityStorageInterface extends FieldableEntityStorageInterface, FieldStorageDefinitionListenerInterface, FieldDefinitionListenerInterface {
- /**
- * Purges a batch of field data.
- *
- * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
- * The deleted field whose data is being purged.
- * @param $batch_size
- * The maximum number of field data records to purge before returning,
- * relating to the count of field data records returned by
- * \Drupal\Core\Entity\FieldableEntityStorageInterface::countFieldData().
- *
- * @return int
- * The number of field data records that have been purged.
- */
- public function purgeFieldData(FieldDefinitionInterface $field_definition, $batch_size);
-
- /**
- * Performs final cleanup after all data of a field has been purged.
- *
- * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
- * The field being purged.
- */
- public function finalizePurge(FieldStorageDefinitionInterface $storage_definition);
-
}
diff --git a/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php b/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php
index e82ed81..0691239 100644
--- a/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php
+++ b/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php
@@ -39,7 +39,7 @@ class EntityAutocomplete extends Textfield {
$info['#validate_reference'] = TRUE;
// IMPORTANT! This should only be set to FALSE if the #default_value
// property is processed at another level (e.g. by a Field API widget) and
- // it's value is properly checked for access.
+ // its value is properly checked for access.
$info['#process_default_value'] = TRUE;
$info['#element_validate'] = [[$class, 'validateEntityAutocomplete']];
@@ -251,8 +251,8 @@ class EntityAutocomplete extends Textfield {
/**
* Finds an entity from an autocomplete input without an explicit ID.
*
- * The method will return an entity ID if one single entity unambuguously
- * matches the incoming input, and sill assign form errors otherwise.
+ * The method will return an entity ID if one single entity unambiguously
+ * matches the incoming input, and assign form errors otherwise.
*
* @param \Drupal\Core\Entity\EntityReferenceSelection\SelectionInterface $handler
* Entity reference selection plugin.
diff --git a/core/lib/Drupal/Core/Entity/Enhancer/EntityRouteEnhancer.php b/core/lib/Drupal/Core/Entity/Enhancer/EntityRouteEnhancer.php
index 850d404..f383fbc 100644
--- a/core/lib/Drupal/Core/Entity/Enhancer/EntityRouteEnhancer.php
+++ b/core/lib/Drupal/Core/Entity/Enhancer/EntityRouteEnhancer.php
@@ -68,7 +68,6 @@ class EntityRouteEnhancer implements EnhancerInterface {
return $defaults;
}
-
/**
* Update defaults for an entity list.
*
diff --git a/core/lib/Drupal/Core/Entity/Entity.php b/core/lib/Drupal/Core/Entity/Entity.php
index 4fe3786..84991bc 100644
--- a/core/lib/Drupal/Core/Entity/Entity.php
+++ b/core/lib/Drupal/Core/Entity/Entity.php
@@ -5,7 +5,6 @@ namespace Drupal\Core\Entity;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\RefinableCacheableDependencyTrait;
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
-use Drupal\Component\Utility\Unicode;
use Drupal\Core\Config\Entity\Exception\ConfigEntityIdLengthException;
use Drupal\Core\Entity\Exception\UndefinedLinkTemplateException;
use Drupal\Core\Language\Language;
@@ -13,6 +12,7 @@ use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Link;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Url;
+use Symfony\Component\Routing\Exception\MissingMandatoryParametersException;
use Symfony\Component\Routing\Exception\RouteNotFoundException;
/**
@@ -344,6 +344,9 @@ abstract class Entity implements EntityInterface {
catch (RouteNotFoundException $e) {
return FALSE;
}
+ catch (MissingMandatoryParametersException $e) {
+ return FALSE;
+ }
return TRUE;
});
}
@@ -427,7 +430,7 @@ abstract class Entity implements EntityInterface {
// Check if this is an entity bundle.
if ($this->getEntityType()->getBundleOf()) {
// Throw an exception if the bundle ID is longer than 32 characters.
- if (Unicode::strlen($this->id()) > EntityTypeInterface::BUNDLE_MAX_LENGTH) {
+ if (mb_strlen($this->id()) > EntityTypeInterface::BUNDLE_MAX_LENGTH) {
throw new ConfigEntityIdLengthException("Attempt to create a bundle with an ID longer than " . EntityTypeInterface::BUNDLE_MAX_LENGTH . " characters: $this->id().");
}
}
diff --git a/core/lib/Drupal/Core/Entity/Entity/EntityFormDisplay.php b/core/lib/Drupal/Core/Entity/Entity/EntityFormDisplay.php
index 6c31df5..183e428 100644
--- a/core/lib/Drupal/Core/Entity/Entity/EntityFormDisplay.php
+++ b/core/lib/Drupal/Core/Entity/Entity/EntityFormDisplay.php
@@ -144,7 +144,7 @@ class EntityFormDisplay extends EntityDisplayBase implements EntityFormDisplayIn
'form_mode' => $this->originalMode,
// No need to prepare, defaults have been merged in setComponent().
'prepare' => FALSE,
- 'configuration' => $configuration
+ 'configuration' => $configuration,
]);
}
else {
@@ -330,7 +330,7 @@ class EntityFormDisplay extends EntityDisplayBase implements EntityFormDisplayIn
}
return [
- 'widgets' => new EntityDisplayPluginCollection($this->pluginManager, $configurations)
+ 'widgets' => new EntityDisplayPluginCollection($this->pluginManager, $configurations),
];
}
diff --git a/core/lib/Drupal/Core/Entity/Entity/EntityViewDisplay.php b/core/lib/Drupal/Core/Entity/Entity/EntityViewDisplay.php
index 1604e31..6783522 100644
--- a/core/lib/Drupal/Core/Entity/Entity/EntityViewDisplay.php
+++ b/core/lib/Drupal/Core/Entity/Entity/EntityViewDisplay.php
@@ -8,7 +8,8 @@ use Drupal\Core\Entity\EntityDisplayPluginCollection;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Entity\EntityDisplayBase;
-use Drupal\Core\TypedData\TranslatableInterface;
+use Drupal\Core\Render\Element;
+use Drupal\Core\TypedData\TranslatableInterface as TranslatableDataInterface;
/**
* Configuration entity that contains display options for all components of a
@@ -201,7 +202,7 @@ class EntityViewDisplay extends EntityDisplayBase implements EntityViewDisplayIn
'view_mode' => $this->originalMode,
// No need to prepare, defaults have been merged in setComponent().
'prepare' => FALSE,
- 'configuration' => $configuration
+ 'configuration' => $configuration,
]);
}
else {
@@ -253,7 +254,7 @@ class EntityViewDisplay extends EntityDisplayBase implements EntityViewDisplayIn
// those values using:
// - the entity language if the entity is translatable,
// - the current "content language" otherwise.
- if ($entity instanceof TranslatableInterface && $entity->isTranslatable()) {
+ if ($entity instanceof TranslatableDataInterface && $entity->isTranslatable()) {
$view_langcode = $entity->language()->getId();
}
else {
@@ -269,7 +270,7 @@ class EntityViewDisplay extends EntityDisplayBase implements EntityViewDisplayIn
foreach ($entities as $id => $entity) {
// Assign the configured weights.
foreach ($this->getComponents() as $name => $options) {
- if (isset($build_list[$id][$name])) {
+ if (isset($build_list[$id][$name]) && !Element::isEmpty($build_list[$id][$name])) {
$build_list[$id][$name]['#weight'] = $options['weight'];
}
}
@@ -301,7 +302,7 @@ class EntityViewDisplay extends EntityDisplayBase implements EntityViewDisplayIn
}
return [
- 'formatters' => new EntityDisplayPluginCollection($this->pluginManager, $configurations)
+ 'formatters' => new EntityDisplayPluginCollection($this->pluginManager, $configurations),
];
}
diff --git a/core/lib/Drupal/Core/Entity/EntityAccessControlHandler.php b/core/lib/Drupal/Core/Entity/EntityAccessControlHandler.php
index ac36411..0a2bb19 100644
--- a/core/lib/Drupal/Core/Entity/EntityAccessControlHandler.php
+++ b/core/lib/Drupal/Core/Entity/EntityAccessControlHandler.php
@@ -158,7 +158,7 @@ class EntityAccessControlHandler extends EntityHandlerBase implements EntityAcce
return AccessResult::forbidden()->addCacheableDependency($entity);
}
if ($admin_permission = $this->entityType->getAdminPermission()) {
- return AccessResult::allowedIfHasPermission($account, $this->entityType->getAdminPermission());
+ return AccessResult::allowedIfHasPermission($account, $admin_permission);
}
else {
// No opinion.
@@ -316,14 +316,18 @@ class EntityAccessControlHandler extends EntityHandlerBase implements EntityAcce
$default = $items ? $items->defaultAccess($operation, $account) : AccessResult::allowed();
// Explicitly disallow changing the entity ID and entity UUID.
- if ($operation === 'edit') {
+ $entity = $items ? $items->getEntity() : NULL;
+ if ($operation === 'edit' && $entity) {
if ($field_definition->getName() === $this->entityType->getKey('id')) {
- return $return_as_object ? AccessResult::forbidden('The entity ID cannot be changed') : FALSE;
+ // String IDs can be set when creating the entity.
+ if (!($entity->isNew() && $field_definition->getType() === 'string')) {
+ return $return_as_object ? AccessResult::forbidden('The entity ID cannot be changed.')->addCacheableDependency($entity) : FALSE;
+ }
}
elseif ($field_definition->getName() === $this->entityType->getKey('uuid')) {
// UUIDs can be set when creating an entity.
- if ($items && ($entity = $items->getEntity()) && !$entity->isNew()) {
- return $return_as_object ? AccessResult::forbidden('The entity UUID cannot be changed')->addCacheableDependency($entity) : FALSE;
+ if (!$entity->isNew()) {
+ return $return_as_object ? AccessResult::forbidden('The entity UUID cannot be changed.')->addCacheableDependency($entity) : FALSE;
}
}
}
diff --git a/core/lib/Drupal/Core/Entity/EntityBundleListener.php b/core/lib/Drupal/Core/Entity/EntityBundleListener.php
index e3b219b..e37317a 100644
--- a/core/lib/Drupal/Core/Entity/EntityBundleListener.php
+++ b/core/lib/Drupal/Core/Entity/EntityBundleListener.php
@@ -68,6 +68,7 @@ class EntityBundleListener implements EntityBundleListenerInterface {
}
// Invoke hook_entity_bundle_create() hook.
$this->moduleHandler->invokeAll('entity_bundle_create', [$entity_type_id, $bundle]);
+ $this->entityFieldManager->clearCachedFieldDefinitions();
}
/**
diff --git a/core/lib/Drupal/Core/Entity/EntityChangedInterface.php b/core/lib/Drupal/Core/Entity/EntityChangedInterface.php
index 7475717..2c92b0c 100644
--- a/core/lib/Drupal/Core/Entity/EntityChangedInterface.php
+++ b/core/lib/Drupal/Core/Entity/EntityChangedInterface.php
@@ -37,6 +37,11 @@ interface EntityChangedInterface {
/**
* Gets the timestamp of the last entity change across all translations.
*
+ * This method will return the highest timestamp across all translations. To
+ * check that no translation is older than in another version of the entity
+ * (e.g. to avoid overwriting newer translations with old data), compare each
+ * translation to the other version individually.
+ *
* @return int
* The timestamp of the last entity save operation across all
* translations.
diff --git a/core/lib/Drupal/Core/Entity/EntityChangesDetectionTrait.php b/core/lib/Drupal/Core/Entity/EntityChangesDetectionTrait.php
new file mode 100644
index 0000000..cde4d83
--- /dev/null
+++ b/core/lib/Drupal/Core/Entity/EntityChangesDetectionTrait.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace Drupal\Core\Entity;
+
+/**
+ * Provides helper methods to detect changes in an entity object.
+ *
+ * @internal This may be replaced by a proper entity comparison handler.
+ */
+trait EntityChangesDetectionTrait {
+
+ /**
+ * Returns an array of field names to skip when checking for changes.
+ *
+ * @param \Drupal\Core\Entity\ContentEntityInterface $entity
+ * A content entity object.
+ *
+ * @return string[]
+ * An array of field names.
+ */
+ protected function getFieldsToSkipFromTranslationChangesCheck(ContentEntityInterface $entity) {
+ /** @var \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type */
+ $entity_type = $entity->getEntityType();
+
+ // A list of known revision metadata fields which should be skipped from
+ // the comparision.
+ $fields = [
+ $entity_type->getKey('revision'),
+ $entity_type->getKey('revision_translation_affected'),
+ ];
+ $fields = array_merge($fields, array_values($entity_type->getRevisionMetadataKeys()));
+
+ // Computed fields should be skipped by the check for translation changes.
+ foreach (array_diff_key($entity->getFieldDefinitions(), array_flip($fields)) as $field_name => $field_definition) {
+ if ($field_definition->isComputed()) {
+ $fields[] = $field_name;
+ }
+ }
+
+ return $fields;
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Entity/EntityCreateAccessCheck.php b/core/lib/Drupal/Core/Entity/EntityCreateAccessCheck.php
index 9ceac52..b3c12a8 100644
--- a/core/lib/Drupal/Core/Entity/EntityCreateAccessCheck.php
+++ b/core/lib/Drupal/Core/Entity/EntityCreateAccessCheck.php
@@ -62,7 +62,7 @@ class EntityCreateAccessCheck implements AccessInterface {
}
// If we were unable to replace all placeholders, deny access.
if (strpos($bundle, '{') !== FALSE) {
- return AccessResult::neutral();
+ return AccessResult::neutral(sprintf("Could not find '%s' request argument, therefore cannot check create access.", $bundle));
}
}
return $this->entityManager->getAccessControlHandler($entity_type)->createAccess($bundle, $account, [], TRUE);
diff --git a/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManager.php b/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManager.php
index 70dec2c..c5d0abd 100644
--- a/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManager.php
+++ b/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManager.php
@@ -22,13 +22,28 @@ class EntityDefinitionUpdateManager implements EntityDefinitionUpdateManagerInte
protected $entityManager;
/**
+ * The last installed schema repository.
+ *
+ * @var \Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface
+ */
+ protected $entityLastInstalledSchemaRepository;
+
+ /**
* Constructs a new EntityDefinitionUpdateManager.
*
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager.
+ * @param \Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface $entity_last_installed_schema_repository
+ * The last installed schema repository service.
*/
- public function __construct(EntityManagerInterface $entity_manager) {
+ public function __construct(EntityManagerInterface $entity_manager, EntityLastInstalledSchemaRepositoryInterface $entity_last_installed_schema_repository = NULL) {
$this->entityManager = $entity_manager;
+
+ if (!isset($entity_last_installed_schema_repository)) {
+ @trigger_error('The $entity_last_installed_schema_repository parameter was added in Drupal 8.6.x and will be required in 9.0.0. See https://www.drupal.org/node/2973262.', E_USER_DEPRECATED);
+ $entity_last_installed_schema_repository = \Drupal::service('entity.last_installed_schema.repository');
+ }
+ $this->entityLastInstalledSchemaRepository = $entity_last_installed_schema_repository;
}
/**
@@ -63,7 +78,7 @@ class EntityDefinitionUpdateManager implements EntityDefinitionUpdateManagerInte
// Process field storage definition changes.
if (!empty($change_list['field_storage_definitions'])) {
$storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id);
- $original_storage_definitions = $this->entityManager->getLastInstalledFieldStorageDefinitions($entity_type_id);
+ $original_storage_definitions = $this->entityLastInstalledSchemaRepository->getLastInstalledFieldStorageDefinitions($entity_type_id);
foreach ($change_list['field_storage_definitions'] as $field_name => $change) {
switch ($change) {
@@ -108,7 +123,7 @@ class EntityDefinitionUpdateManager implements EntityDefinitionUpdateManagerInte
// Process field storage definition changes.
if (!empty($change_list['field_storage_definitions'])) {
$storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id);
- $original_storage_definitions = $this->entityManager->getLastInstalledFieldStorageDefinitions($entity_type_id);
+ $original_storage_definitions = $this->entityLastInstalledSchemaRepository->getLastInstalledFieldStorageDefinitions($entity_type_id);
foreach ($change_list['field_storage_definitions'] as $field_name => $change) {
$storage_definition = isset($storage_definitions[$field_name]) ? $storage_definitions[$field_name] : NULL;
@@ -123,13 +138,20 @@ class EntityDefinitionUpdateManager implements EntityDefinitionUpdateManagerInte
* {@inheritdoc}
*/
public function getEntityType($entity_type_id) {
- $entity_type = $this->entityManager->getLastInstalledDefinition($entity_type_id);
+ $entity_type = $this->entityLastInstalledSchemaRepository->getLastInstalledDefinition($entity_type_id);
return $entity_type ? clone $entity_type : NULL;
}
/**
* {@inheritdoc}
*/
+ public function getEntityTypes() {
+ return $this->entityLastInstalledSchemaRepository->getLastInstalledDefinitions();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
public function installEntityType(EntityTypeInterface $entity_type) {
$this->entityManager->clearCachedDefinitions();
$this->entityManager->onEntityTypeCreate($entity_type);
@@ -173,7 +195,7 @@ class EntityDefinitionUpdateManager implements EntityDefinitionUpdateManagerInte
* {@inheritdoc}
*/
public function getFieldStorageDefinition($name, $entity_type_id) {
- $storage_definitions = $this->entityManager->getLastInstalledFieldStorageDefinitions($entity_type_id);
+ $storage_definitions = $this->entityLastInstalledSchemaRepository->getLastInstalledFieldStorageDefinitions($entity_type_id);
return isset($storage_definitions[$name]) ? clone $storage_definitions[$name] : NULL;
}
@@ -211,7 +233,7 @@ class EntityDefinitionUpdateManager implements EntityDefinitionUpdateManagerInte
break;
case static::DEFINITION_UPDATED:
- $original = $this->entityManager->getLastInstalledDefinition($entity_type_id);
+ $original = $this->entityLastInstalledSchemaRepository->getLastInstalledDefinition($entity_type_id);
$this->entityManager->onEntityTypeUpdate($entity_type, $original);
break;
}
@@ -262,7 +284,7 @@ class EntityDefinitionUpdateManager implements EntityDefinitionUpdateManagerInte
$change_list = [];
foreach ($this->entityManager->getDefinitions() as $entity_type_id => $entity_type) {
- $original = $this->entityManager->getLastInstalledDefinition($entity_type_id);
+ $original = $this->entityLastInstalledSchemaRepository->getLastInstalledDefinition($entity_type_id);
// @todo Support non-storage-schema-changing definition updates too:
// https://www.drupal.org/node/2336895.
@@ -277,7 +299,7 @@ class EntityDefinitionUpdateManager implements EntityDefinitionUpdateManagerInte
if ($this->entityManager->getStorage($entity_type_id) instanceof DynamicallyFieldableEntityStorageInterface) {
$field_changes = [];
$storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id);
- $original_storage_definitions = $this->entityManager->getLastInstalledFieldStorageDefinitions($entity_type_id);
+ $original_storage_definitions = $this->entityLastInstalledSchemaRepository->getLastInstalledFieldStorageDefinitions($entity_type_id);
// Detect created field storage definitions.
foreach (array_diff_key($storage_definitions, $original_storage_definitions) as $field_name => $storage_definition) {
@@ -311,7 +333,8 @@ class EntityDefinitionUpdateManager implements EntityDefinitionUpdateManagerInte
}
// @todo Support deleting entity definitions when we support base field
- // purging. See https://www.drupal.org/node/2282119.
+ // purging.
+ // @see https://www.drupal.org/node/2907779
$this->entityManager->useCaches(TRUE);
diff --git a/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManagerInterface.php b/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManagerInterface.php
index 7754170..a219f3c 100644
--- a/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManagerInterface.php
+++ b/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManagerInterface.php
@@ -109,6 +109,18 @@ interface EntityDefinitionUpdateManagerInterface {
public function getEntityType($entity_type_id);
/**
+ * Returns all the entity type definitions, ready to be manipulated.
+ *
+ * When needing to apply updates to existing entity type definitions, this
+ * method should always be used to retrieve all the definitions ready to be
+ * manipulated.
+ *
+ * @return \Drupal\Core\Entity\EntityTypeInterface[]
+ * The last installed entity type definitions, keyed by the entity type ID.
+ */
+ public function getEntityTypes();
+
+ /**
* Installs a new entity type definition.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
diff --git a/core/lib/Drupal/Core/Entity/EntityDeleteFormTrait.php b/core/lib/Drupal/Core/Entity/EntityDeleteFormTrait.php
index 2faf45c..9761190 100644
--- a/core/lib/Drupal/Core/Entity/EntityDeleteFormTrait.php
+++ b/core/lib/Drupal/Core/Entity/EntityDeleteFormTrait.php
@@ -120,7 +120,7 @@ trait EntityDeleteFormTrait {
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->getEntity()->delete();
- drupal_set_message($this->getDeletionMessage());
+ $this->messenger()->addStatus($this->getDeletionMessage());
$form_state->setRedirectUrl($this->getCancelUrl());
$this->logDeletionMessage();
}
diff --git a/core/lib/Drupal/Core/Entity/EntityDeleteMultipleAccessCheck.php b/core/lib/Drupal/Core/Entity/EntityDeleteMultipleAccessCheck.php
new file mode 100644
index 0000000..470c1b0
--- /dev/null
+++ b/core/lib/Drupal/Core/Entity/EntityDeleteMultipleAccessCheck.php
@@ -0,0 +1,86 @@
+<?php
+
+namespace Drupal\Core\Entity;
+
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Routing\Access\AccessInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\TempStore\PrivateTempStoreFactory;
+use Symfony\Component\HttpFoundation\RequestStack;
+
+/**
+ * Checks if the current user has delete access to the items of the tempstore.
+ */
+class EntityDeleteMultipleAccessCheck implements AccessInterface {
+
+ /**
+ * The entity type manager.
+ *
+ * @var \Drupal\Core\Entity\EntityManagerInterface
+ */
+ protected $entityTypeManager;
+
+ /**
+ * The tempstore service.
+ *
+ * @var \Drupal\Core\TempStore\PrivateTempStoreFactory
+ */
+ protected $tempStore;
+
+ /**
+ * Request stack service.
+ *
+ * @var \Symfony\Component\HttpFoundation\RequestStack
+ */
+ protected $requestStack;
+
+ /**
+ * Constructs a new EntityDeleteMultipleAccessCheck.
+ *
+ * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+ * The entity type manager.
+ * @param \Drupal\Core\TempStore\PrivateTempStoreFactory $temp_store_factory
+ * The tempstore service.
+ * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
+ * The request stack service.
+ */
+ public function __construct(EntityTypeManagerInterface $entity_type_manager, PrivateTempStoreFactory $temp_store_factory, RequestStack $request_stack) {
+ $this->entityTypeManager = $entity_type_manager;
+ $this->tempStore = $temp_store_factory->get('entity_delete_multiple_confirm');
+ $this->requestStack = $request_stack;
+ }
+
+ /**
+ * Checks if the user has delete access for at least one item of the store.
+ *
+ * @param \Drupal\Core\Session\AccountInterface $account
+ * Run access checks for this account.
+ * @param string $entity_type_id
+ * Entity type ID.
+ *
+ * @return \Drupal\Core\Access\AccessResult
+ * Allowed or forbidden, neutral if tempstore is empty.
+ */
+ public function access(AccountInterface $account, $entity_type_id) {
+ if (!$this->requestStack->getCurrentRequest()->getSession()) {
+ return AccessResult::neutral();
+ }
+ $selection = $this->tempStore->get($account->id() . ':' . $entity_type_id);
+ if (empty($selection) || !is_array($selection)) {
+ return AccessResult::neutral();
+ }
+
+ $entities = $this->entityTypeManager->getStorage($entity_type_id)->loadMultiple(array_keys($selection));
+ foreach ($entities as $entity) {
+ // As long as the user has access to delete one entity allow access to the
+ // delete form. Access will be checked again in
+ // Drupal\Core\Entity\Form\DeleteMultipleForm::submit() in case it has
+ // changed in the meantime.
+ if ($entity->access('delete', $account)) {
+ return AccessResult::allowed();
+ }
+ }
+ return AccessResult::forbidden();
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Entity/EntityDisplayBase.php b/core/lib/Drupal/Core/Entity/EntityDisplayBase.php
index ca106b2..dbda544 100644
--- a/core/lib/Drupal/Core/Entity/EntityDisplayBase.php
+++ b/core/lib/Drupal/Core/Entity/EntityDisplayBase.php
@@ -163,12 +163,12 @@ abstract class EntityDisplayBase extends ConfigEntityBase implements EntityDispl
if (!isset($this->content[$name]) && !isset($this->hidden[$name])) {
// Extra fields are visible by default unless they explicitly say so.
if (!isset($definition['visible']) || $definition['visible'] == TRUE) {
- $this->content[$name] = [
- 'weight' => $definition['weight']
- ];
+ $this->setComponent($name, [
+ 'weight' => $definition['weight'],
+ ]);
}
else {
- $this->hidden[$name] = TRUE;
+ $this->removeComponent($name);
}
}
// Ensure extra fields have a 'region'.
@@ -190,11 +190,11 @@ abstract class EntityDisplayBase extends ConfigEntityBase implements EntityDispl
}
if (!empty($options['region']) && $options['region'] === 'hidden') {
- $this->hidden[$name] = TRUE;
+ $this->removeComponent($name);
}
elseif ($options) {
$options += ['region' => $default_region];
- $this->content[$name] = $this->pluginManager->prepareConfiguration($definition->getType(), $options);
+ $this->setComponent($name, $options);
}
// Note: (base) fields that do not specify display options are not
// tracked in the display at all, in order to avoid cluttering the
@@ -250,7 +250,7 @@ abstract class EntityDisplayBase extends ConfigEntityBase implements EntityDispl
/**
* {@inheritdoc}
*/
- public function preSave(EntityStorageInterface $storage, $update = TRUE) {
+ public function preSave(EntityStorageInterface $storage) {
// Ensure that a region is set on each component.
foreach ($this->getComponents() as $name => $component) {
$this->handleHiddenType($name, $component);
@@ -263,7 +263,7 @@ abstract class EntityDisplayBase extends ConfigEntityBase implements EntityDispl
ksort($this->content);
ksort($this->hidden);
- parent::preSave($storage, $update);
+ parent::preSave($storage);
}
/**
@@ -439,7 +439,7 @@ abstract class EntityDisplayBase extends ConfigEntityBase implements EntityDispl
/**
* Determines if a field has options for a given display.
*
- * @param FieldDefinitionInterface $definition
+ * @param \Drupal\Core\Field\FieldDefinitionInterface $definition
* A field definition.
* @return array|null
*/
diff --git a/core/lib/Drupal/Core/Entity/EntityFieldManager.php b/core/lib/Drupal/Core/Entity/EntityFieldManager.php
index d6e70ee..88186c6 100644
--- a/core/lib/Drupal/Core/Entity/EntityFieldManager.php
+++ b/core/lib/Drupal/Core/Entity/EntityFieldManager.php
@@ -60,7 +60,7 @@ class EntityFieldManager implements EntityFieldManagerInterface {
* - type: The field type.
* - bundles: The bundles in which the field appears.
*
- * @return array
+ * @var array
*/
protected $fieldMap = [];
@@ -190,8 +190,10 @@ class EntityFieldManager implements EntityFieldManagerInterface {
* flagged as translatable.
*/
protected function buildBaseFieldDefinitions($entity_type_id) {
+ /** @var \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type */
$entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
$class = $entity_type->getClass();
+ /** @var string[] $keys */
$keys = array_filter($entity_type->getKeys());
// Fail with an exception for non-fieldable entity types.
@@ -221,6 +223,21 @@ class EntityFieldManager implements EntityFieldManagerInterface {
}
// Make sure that revisionable entity types are correctly defined.
+ if ($entity_type->isRevisionable()) {
+ // Disable the BC layer to prevent a recursion, this only needs the
+ // revision_default key that is always set.
+ $field_name = $entity_type->getRevisionMetadataKeys(FALSE)['revision_default'];
+ $base_field_definitions[$field_name] = BaseFieldDefinition::create('boolean')
+ ->setLabel($this->t('Default revision'))
+ ->setDescription($this->t('A flag indicating whether this was a default revision when it was saved.'))
+ ->setStorageRequired(TRUE)
+ ->setInternal(TRUE)
+ ->setTranslatable(FALSE)
+ ->setRevisionable(TRUE);
+ }
+
+ // Make sure that revisionable and translatable entity types are correctly
+ // defined.
if ($entity_type->isRevisionable() && $entity_type->isTranslatable()) {
// The 'revision_translation_affected' field should always be defined.
// This field has been added unconditionally in Drupal 8.4.0 and it is
@@ -480,7 +497,7 @@ class EntityFieldManager implements EntityFieldManagerInterface {
}
}
- $this->cacheSet($cid, $this->fieldMap, Cache::PERMANENT, ['entity_types']);
+ $this->cacheSet($cid, $this->fieldMap, Cache::PERMANENT, ['entity_types', 'entity_field_info']);
}
}
return $this->fieldMap;
diff --git a/core/lib/Drupal/Core/Entity/EntityForm.php b/core/lib/Drupal/Core/Entity/EntityForm.php
index 25b837b..7965028 100644
--- a/core/lib/Drupal/Core/Entity/EntityForm.php
+++ b/core/lib/Drupal/Core/Entity/EntityForm.php
@@ -221,6 +221,17 @@ class EntityForm extends FormBase implements EntityFormInterface {
/**
* Returns an array of supported actions for the current entity form.
*
+ * This function generates a list of Form API elements which represent
+ * actions supported by the current entity form.
+ *
+ * @param array $form
+ * An associative array containing the structure of the form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the form.
+ *
+ * @return array
+ * An array of supported Form API action elements keyed by name.
+ *
* @todo Consider introducing a 'preview' action here, since it is used by
* many entity types.
*/
diff --git a/core/lib/Drupal/Core/Entity/EntityLastInstalledSchemaRepository.php b/core/lib/Drupal/Core/Entity/EntityLastInstalledSchemaRepository.php
index 6130012..70e552c 100644
--- a/core/lib/Drupal/Core/Entity/EntityLastInstalledSchemaRepository.php
+++ b/core/lib/Drupal/Core/Entity/EntityLastInstalledSchemaRepository.php
@@ -37,6 +37,28 @@ class EntityLastInstalledSchemaRepository implements EntityLastInstalledSchemaRe
/**
* {@inheritdoc}
*/
+ public function getLastInstalledDefinitions() {
+ $all_definitions = $this->keyValueFactory->get('entity.definitions.installed')->getAll();
+
+ // Filter out field storage definitions.
+ $filtered_keys = array_filter(array_keys($all_definitions), function ($key) {
+ return substr($key, -12) === '.entity_type';
+ });
+ $entity_type_definitions = array_intersect_key($all_definitions, array_flip($filtered_keys));
+
+ // Ensure that the returned array is keyed by the entity type ID.
+ $keys = array_keys($entity_type_definitions);
+ $keys = array_map(function ($key) {
+ $parts = explode('.', $key);
+ return $parts[0];
+ }, $keys);
+
+ return array_combine($keys, $entity_type_definitions);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
public function setLastInstalledDefinition(EntityTypeInterface $entity_type) {
$entity_type_id = $entity_type->id();
$this->keyValueFactory->get('entity.definitions.installed')->set($entity_type_id . '.entity_type', $entity_type);
diff --git a/core/lib/Drupal/Core/Entity/EntityLastInstalledSchemaRepositoryInterface.php b/core/lib/Drupal/Core/Entity/EntityLastInstalledSchemaRepositoryInterface.php
index 53f02a0..af910bb 100644
--- a/core/lib/Drupal/Core/Entity/EntityLastInstalledSchemaRepositoryInterface.php
+++ b/core/lib/Drupal/Core/Entity/EntityLastInstalledSchemaRepositoryInterface.php
@@ -42,6 +42,34 @@ interface EntityLastInstalledSchemaRepositoryInterface {
public function getLastInstalledDefinition($entity_type_id);
/**
+ * Gets the entity type definitions in their most recently installed state.
+ *
+ * During the application lifetime, entity type definitions can change. For
+ * example, updated code can be deployed. The
+ * \Drupal\Core\Entity\EntityTypeManagerInterface::getDefinitions() method
+ * will always return the definitions as determined by the current codebase.
+ * This method returns the definitions from the last time that a
+ * \Drupal\Core\Entity\EntityTypeListener event was completed. In other words,
+ * the definitions that the entity type's handlers have incorporated into the
+ * application state. For example, if the entity type's storage handler is
+ * SQL-based, the definition for which database tables were created.
+ *
+ * Application management code can check if
+ * \Drupal\Core\Entity\EntityTypeManagerInterface::getDefinitions() differs
+ * from getLastInstalledDefinitions() and decide whether to:
+ * - Invoke the appropriate \Drupal\Core\Entity\EntityTypeListenerInterface
+ * event so that handlers react to the new definitions.
+ * - Raise a warning that the application state is incompatible with the
+ * codebase.
+ * - Perform some other action.
+ *
+ * @return \Drupal\Core\Entity\EntityTypeInterface[]
+ * An array containing the installed definition for all entity types, keyed
+ * by the entity type ID.
+ */
+ public function getLastInstalledDefinitions();
+
+ /**
* Stores the entity type definition in the application state.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
diff --git a/core/lib/Drupal/Core/Entity/EntityListBuilder.php b/core/lib/Drupal/Core/Entity/EntityListBuilder.php
index 7191fae..c784351 100644
--- a/core/lib/Drupal/Core/Entity/EntityListBuilder.php
+++ b/core/lib/Drupal/Core/Entity/EntityListBuilder.php
@@ -2,6 +2,7 @@
namespace Drupal\Core\Entity;
+use Drupal\Core\Messenger\MessengerTrait;
use Drupal\Core\Routing\RedirectDestinationTrait;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -13,6 +14,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
*/
class EntityListBuilder extends EntityHandlerBase implements EntityListBuilderInterface, EntityHandlerInterface {
+ use MessengerTrait;
use RedirectDestinationTrait;
/**
@@ -223,7 +225,7 @@ class EntityListBuilder extends EntityHandlerBase implements EntityListBuilderIn
'#header' => $this->buildHeader(),
'#title' => $this->getTitle(),
'#rows' => [],
- '#empty' => $this->t('There is no @label yet.', ['@label' => $this->entityType->getLabel()]),
+ '#empty' => $this->t('There are no @label yet.', ['@label' => $this->entityType->getPluralLabel()]),
'#cache' => [
'contexts' => $this->entityType->getListCacheContexts(),
'tags' => $this->entityType->getListCacheTags(),
diff --git a/core/lib/Drupal/Core/Entity/EntityListBuilderInterface.php b/core/lib/Drupal/Core/Entity/EntityListBuilderInterface.php
index bab9d97..eb16874 100644
--- a/core/lib/Drupal/Core/Entity/EntityListBuilderInterface.php
+++ b/core/lib/Drupal/Core/Entity/EntityListBuilderInterface.php
@@ -47,7 +47,8 @@ interface EntityListBuilderInterface {
* Builds a listing of entities for the given entity type.
*
* @return array
- * A render array as expected by drupal_render().
+ * A render array as expected by
+ * \Drupal\Core\Render\RendererInterface::render().
*/
public function render();
diff --git a/core/lib/Drupal/Core/Entity/EntityManager.php b/core/lib/Drupal/Core/Entity/EntityManager.php
index dbd3b91..bc03175 100644
--- a/core/lib/Drupal/Core/Entity/EntityManager.php
+++ b/core/lib/Drupal/Core/Entity/EntityManager.php
@@ -303,7 +303,7 @@ class EntityManager implements EntityManagerInterface, ContainerAwareInterface {
* {@inheritdoc}
*
* @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0.
- * Use \Drupal\Core\Entity\EntityTypeBundleManagerInterface::clearCachedBundles()
+ * Use \Drupal\Core\Entity\EntityTypeBundleInfoInterface::clearCachedBundles()
* instead.
*
* @see https://www.drupal.org/node/2549139
@@ -316,7 +316,7 @@ class EntityManager implements EntityManagerInterface, ContainerAwareInterface {
* {@inheritdoc}
*
* @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0.
- * Use \Drupal\Core\Entity\EntityTypeBundleManagerInterface::getBundleInfo()
+ * Use \Drupal\Core\Entity\EntityTypeBundleInfoInterface::getBundleInfo()
* instead.
*
* @see https://www.drupal.org/node/2549139
@@ -329,7 +329,7 @@ class EntityManager implements EntityManagerInterface, ContainerAwareInterface {
* {@inheritdoc}
*
* @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0.
- * Use \Drupal\Core\Entity\EntityTypeBundleManagerInterface::getAllBundleInfo()
+ * Use \Drupal\Core\Entity\EntityTypeBundleInfoInterface::getAllBundleInfo()
* instead.
*
* @see https://www.drupal.org/node/2549139
diff --git a/core/lib/Drupal/Core/Entity/EntityPublishedTrait.php b/core/lib/Drupal/Core/Entity/EntityPublishedTrait.php
index eb4d82e..1f36625 100644
--- a/core/lib/Drupal/Core/Entity/EntityPublishedTrait.php
+++ b/core/lib/Drupal/Core/Entity/EntityPublishedTrait.php
@@ -45,8 +45,7 @@ trait EntityPublishedTrait {
* {@inheritdoc}
*/
public function isPublished() {
- $key = $this->getEntityType()->getKey('published');
- return (bool) $this->get($key)->value;
+ return (bool) $this->getEntityKey('published');
}
/**
diff --git a/core/lib/Drupal/Core/Entity/EntityRepository.php b/core/lib/Drupal/Core/Entity/EntityRepository.php
index 986cc50..37a89d79 100644
--- a/core/lib/Drupal/Core/Entity/EntityRepository.php
+++ b/core/lib/Drupal/Core/Entity/EntityRepository.php
@@ -5,7 +5,7 @@ namespace Drupal\Core\Entity;
use Drupal\Core\Config\Entity\ConfigEntityTypeInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Language\LanguageManagerInterface;
-use Drupal\Core\TypedData\TranslatableInterface;
+use Drupal\Core\TypedData\TranslatableInterface as TranslatableDataInterface;
/**
* Provides several mechanisms for retrieving entities.
@@ -82,7 +82,7 @@ class EntityRepository implements EntityRepositoryInterface {
public function getTranslationFromContext(EntityInterface $entity, $langcode = NULL, $context = []) {
$translation = $entity;
- if ($entity instanceof TranslatableInterface && count($entity->getTranslationLanguages()) > 1) {
+ if ($entity instanceof TranslatableDataInterface && count($entity->getTranslationLanguages()) > 1) {
if (empty($langcode)) {
$langcode = $this->languageManager->getCurrentLanguage(LanguageInterface::TYPE_CONTENT)->getId();
$entity->addCacheContexts(['languages:' . LanguageInterface::TYPE_CONTENT]);
diff --git a/core/lib/Drupal/Core/Entity/EntityStorageBase.php b/core/lib/Drupal/Core/Entity/EntityStorageBase.php
index e5bf880..d7263dc 100644
--- a/core/lib/Drupal/Core/Entity/EntityStorageBase.php
+++ b/core/lib/Drupal/Core/Entity/EntityStorageBase.php
@@ -3,6 +3,7 @@
namespace Drupal\Core\Entity;
use Drupal\Core\Entity\Query\QueryInterface;
+use Drupal\Core\Cache\MemoryCache\MemoryCacheInterface;
/**
* A base entity storage class.
@@ -10,13 +11,6 @@ use Drupal\Core\Entity\Query\QueryInterface;
abstract class EntityStorageBase extends EntityHandlerBase implements EntityStorageInterface, EntityHandlerInterface {
/**
- * Static cache of entities, keyed by entity ID.
- *
- * @var array
- */
- protected $entities = [];
-
- /**
* Entity type ID for this storage.
*
* @var string
@@ -73,18 +67,41 @@ abstract class EntityStorageBase extends EntityHandlerBase implements EntityStor
protected $entityClass;
/**
+ * The memory cache.
+ *
+ * @var \Drupal\Core\Cache\MemoryCache\MemoryCacheInterface
+ */
+ protected $memoryCache;
+
+ /**
+ * The memory cache cache tag.
+ *
+ * @var string
+ */
+ protected $memoryCacheTag;
+
+ /**
* Constructs an EntityStorageBase instance.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
+ * @param \Drupal\Core\Cache\MemoryCache\MemoryCacheInterface|null $memory_cache
+ * The memory cache.
*/
- public function __construct(EntityTypeInterface $entity_type) {
+ public function __construct(EntityTypeInterface $entity_type, MemoryCacheInterface $memory_cache = NULL) {
$this->entityTypeId = $entity_type->id();
$this->entityType = $entity_type;
$this->idKey = $this->entityType->getKey('id');
$this->uuidKey = $this->entityType->getKey('uuid');
$this->langcodeKey = $this->entityType->getKey('langcode');
$this->entityClass = $this->entityType->getClass();
+
+ if (!isset($memory_cache)) {
+ @trigger_error('The $memory_cache parameter was added in Drupal 8.6.x and will be required in 9.0.0. See https://www.drupal.org/node/2973262', E_USER_DEPRECATED);
+ $memory_cache = \Drupal::service('entity.memory_cache');
+ }
+ $this->memoryCache = $memory_cache;
+ $this->memoryCacheTag = 'entity.memory_cache:' . $this->entityTypeId;
}
/**
@@ -102,6 +119,19 @@ abstract class EntityStorageBase extends EntityHandlerBase implements EntityStor
}
/**
+ * Builds the cache ID for the passed in entity ID.
+ *
+ * @param int $id
+ * Entity ID for which the cache ID should be built.
+ *
+ * @return string
+ * Cache ID that can be passed to the cache backend.
+ */
+ protected function buildCacheId($id) {
+ return "values:{$this->entityTypeId}:$id";
+ }
+
+ /**
* {@inheritdoc}
*/
public function loadUnchanged($id) {
@@ -115,11 +145,12 @@ abstract class EntityStorageBase extends EntityHandlerBase implements EntityStor
public function resetCache(array $ids = NULL) {
if ($this->entityType->isStaticallyCacheable() && isset($ids)) {
foreach ($ids as $id) {
- unset($this->entities[$id]);
+ $this->memoryCache->delete($this->buildCacheId($id));
}
}
else {
- $this->entities = [];
+ // Call the backend method directly.
+ $this->memoryCache->invalidateTags([$this->memoryCacheTag]);
}
}
@@ -135,8 +166,12 @@ abstract class EntityStorageBase extends EntityHandlerBase implements EntityStor
protected function getFromStaticCache(array $ids) {
$entities = [];
// Load any available entities from the internal cache.
- if ($this->entityType->isStaticallyCacheable() && !empty($this->entities)) {
- $entities += array_intersect_key($this->entities, array_flip($ids));
+ if ($this->entityType->isStaticallyCacheable()) {
+ foreach ($ids as $id) {
+ if ($cached = $this->memoryCache->get($this->buildCacheId($id))) {
+ $entities[$id] = $cached->data;
+ }
+ }
}
return $entities;
}
@@ -149,7 +184,9 @@ abstract class EntityStorageBase extends EntityHandlerBase implements EntityStor
*/
protected function setStaticCache(array $entities) {
if ($this->entityType->isStaticallyCacheable()) {
- $this->entities += $entities;
+ foreach ($entities as $id => $entity) {
+ $this->memoryCache->set($this->buildCacheId($entity->id()), $entity, MemoryCacheInterface::CACHE_PERMANENT, [$this->memoryCacheTag]);
+ }
}
}
@@ -157,7 +194,7 @@ abstract class EntityStorageBase extends EntityHandlerBase implements EntityStor
* Invokes a hook on behalf of the entity.
*
* @param string $hook
- * One of 'presave', 'insert', 'update', 'predelete', 'delete', or
+ * One of 'create', 'presave', 'insert', 'update', 'predelete', 'delete', or
* 'revision_delete'.
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity object.
diff --git a/core/lib/Drupal/Core/Entity/EntityStorageInterface.php b/core/lib/Drupal/Core/Entity/EntityStorageInterface.php
index 9f50674..1db2739 100644
--- a/core/lib/Drupal/Core/Entity/EntityStorageInterface.php
+++ b/core/lib/Drupal/Core/Entity/EntityStorageInterface.php
@@ -79,6 +79,12 @@ interface EntityStorageInterface {
*
* @return \Drupal\Core\Entity\EntityInterface|null
* The specified entity revision or NULL if not found.
+ *
+ * @todo Deprecated in Drupal 8.5.0 and will be removed before Drupal 9.0.0.
+ * Use \Drupal\Core\Entity\RevisionableStorageInterface instead.
+ *
+ * @see https://www.drupal.org/node/2926958
+ * @see https://www.drupal.org/node/2927226
*/
public function loadRevision($revision_id);
@@ -89,6 +95,12 @@ interface EntityStorageInterface {
*
* @param int $revision_id
* The revision id.
+ *
+ * @todo Deprecated in Drupal 8.5.0 and will be removed before Drupal 9.0.0.
+ * Use \Drupal\Core\Entity\RevisionableStorageInterface instead.
+ *
+ * @see https://www.drupal.org/node/2926958
+ * @see https://www.drupal.org/node/2927226
*/
public function deleteRevision($revision_id);
diff --git a/core/lib/Drupal/Core/Entity/EntityType.php b/core/lib/Drupal/Core/Entity/EntityType.php
index 9584ed3..c6f320f 100644
--- a/core/lib/Drupal/Core/Entity/EntityType.php
+++ b/core/lib/Drupal/Core/Entity/EntityType.php
@@ -3,7 +3,7 @@
namespace Drupal\Core\Entity;
use Drupal\Component\Plugin\Definition\PluginDefinition;
-use Drupal\Component\Utility\Unicode;
+use Drupal\Core\DependencyInjection\DependencySerializationTrait;
use Drupal\Core\Entity\Exception\EntityTypeIdLengthException;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslatableMarkup;
@@ -15,6 +15,7 @@ use Drupal\Core\StringTranslation\TranslatableMarkup;
*/
class EntityType extends PluginDefinition implements EntityTypeInterface {
+ use DependencySerializationTrait;
use StringTranslationTrait;
/**
@@ -155,6 +156,13 @@ class EntityType extends PluginDefinition implements EntityTypeInterface {
protected $data_table = NULL;
/**
+ * Indicates whether the entity data is internal.
+ *
+ * @var bool
+ */
+ protected $internal = FALSE;
+
+ /**
* Indicates whether entities of this type have multilingual support.
*
* @var bool
@@ -288,7 +296,7 @@ class EntityType extends PluginDefinition implements EntityTypeInterface {
*/
public function __construct($definition) {
// Throw an exception if the entity type ID is longer than 32 characters.
- if (Unicode::strlen($definition['id']) > static::ID_MAX_LENGTH) {
+ if (mb_strlen($definition['id']) > static::ID_MAX_LENGTH) {
throw new EntityTypeIdLengthException('Attempt to create an entity type with an ID longer than ' . static::ID_MAX_LENGTH . " characters: {$definition['id']}.");
}
@@ -311,11 +319,16 @@ class EntityType extends PluginDefinition implements EntityTypeInterface {
$this->checkStorageClass($this->handlers['storage']);
}
- // Automatically add the EntityChanged constraint if the entity type tracks
- // the changed time.
+ // Automatically add the "EntityChanged" constraint if the entity type
+ // tracks the changed time.
if ($this->entityClassImplements(EntityChangedInterface::class)) {
$this->addConstraint('EntityChanged');
}
+ // Automatically add the "EntityUntranslatableFields" constraint if we have
+ // an entity type supporting translatable fields and pending revisions.
+ if ($this->entityClassImplements(ContentEntityInterface::class)) {
+ $this->addConstraint('EntityUntranslatableFields');
+ }
// Ensure a default list cache tag is set.
if (empty($this->list_cache_tags)) {
@@ -352,6 +365,13 @@ class EntityType extends PluginDefinition implements EntityTypeInterface {
/**
* {@inheritdoc}
*/
+ public function isInternal() {
+ return $this->internal;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
public function isStaticallyCacheable() {
return $this->static_cache;
}
@@ -677,7 +697,15 @@ class EntityType extends PluginDefinition implements EntityTypeInterface {
* {@inheritdoc}
*/
public function getBundleLabel() {
- return (string) $this->bundle_label;
+ // If there is no bundle label defined, try to provide some sensible
+ // fallbacks.
+ if (!empty($this->bundle_label)) {
+ return (string) $this->bundle_label;
+ }
+ elseif ($bundle_entity_type_id = $this->getBundleEntityType()) {
+ return (string) \Drupal::entityTypeManager()->getDefinition($bundle_entity_type_id)->getLabel();
+ }
+ return (string) new TranslatableMarkup('@type_label bundle', ['@type_label' => $this->getLabel()], [], $this->getStringTranslation());
}
/**
@@ -741,7 +769,7 @@ class EntityType extends PluginDefinition implements EntityTypeInterface {
* {@inheritdoc}
*/
public function getLowercaseLabel() {
- return Unicode::strtolower($this->getLabel());
+ return mb_strtolower($this->getLabel());
}
/**
@@ -810,7 +838,6 @@ class EntityType extends PluginDefinition implements EntityTypeInterface {
return $this->group;
}
-
/**
* {@inheritdoc}
*/
diff --git a/core/lib/Drupal/Core/Entity/EntityTypeInterface.php b/core/lib/Drupal/Core/Entity/EntityTypeInterface.php
index c8a28b4..b2c1f5d 100644
--- a/core/lib/Drupal/Core/Entity/EntityTypeInterface.php
+++ b/core/lib/Drupal/Core/Entity/EntityTypeInterface.php
@@ -7,10 +7,19 @@ use Drupal\Component\Plugin\Definition\PluginDefinitionInterface;
/**
* Provides an interface for an entity type and its metadata.
*
- * Additional information can be provided by modules: hook_entity_type_build() can be
- * implemented to define new properties, while hook_entity_type_alter() can be
- * implemented to alter existing data and fill-in defaults. Module-specific
- * properties should be documented in the hook implementations defining them.
+ * Entity type classes can provide docblock annotations. The entity type manager
+ * will use these annotations to populate the entity type object with
+ * properties.
+ *
+ * Additional properties can be defined by module implementations of
+ * hook_entity_type_build(). Existing data can be altered in implementations of
+ * hook_entity_type_alter(), which can also be used to fill in defaults.
+ * Module-specific properties should be documented in the hook implementations
+ * defining them.
+ *
+ * @see \Drupal\Core\Entity\EntityTypeManagerInterface
+ * @see hook_entity_type_build()
+ * @see hook_entity_type_alter()
*/
interface EntityTypeInterface extends PluginDefinitionInterface {
@@ -326,10 +335,10 @@ interface EntityTypeInterface extends PluginDefinitionInterface {
public function getAccessControlClass();
/**
- * Gets the access class.
+ * Sets the access control handler class.
*
* @param string $class
- * The class for this entity type's access.
+ * The class for this entity type's access control handler.
*
* @return $this
*/
@@ -547,8 +556,8 @@ interface EntityTypeInterface extends PluginDefinitionInterface {
/**
* Gets the label for the bundle.
*
- * @return string|null
- * The bundle label, or NULL if none exists.
+ * @return string
+ * The bundle label.
*/
public function getBundleLabel();
@@ -563,6 +572,24 @@ interface EntityTypeInterface extends PluginDefinitionInterface {
public function getBaseTable();
/**
+ * Indicates whether the entity data is internal.
+ *
+ * This can be used in a scenario when it is not desirable to expose data of
+ * this entity type to an external system.
+ *
+ * The implications of this method are left to the discretion of the caller.
+ * For example, a module providing an HTTP API may not expose entities of
+ * this type or a custom entity reference field settings form may deprioritize
+ * entities of this type in a select list.
+ *
+ * @return bool
+ * TRUE if the entity data is internal, FALSE otherwise.
+ *
+ * @see \Drupal\Core\TypedData\DataDefinitionInterface::isInternal()
+ */
+ public function isInternal();
+
+ /**
* Indicates whether entities of this type have multilingual support.
*
* At an entity level, this indicates language support and at a bundle level
diff --git a/core/lib/Drupal/Core/Entity/EntityTypeManager.php b/core/lib/Drupal/Core/Entity/EntityTypeManager.php
index abe96a5..9d7fdc5 100644
--- a/core/lib/Drupal/Core/Entity/EntityTypeManager.php
+++ b/core/lib/Drupal/Core/Entity/EntityTypeManager.php
@@ -148,6 +148,7 @@ class EntityTypeManager extends DefaultPluginManager implements EntityTypeManage
parent::useCaches($use_caches);
if (!$use_caches) {
$this->handlers = [];
+ $this->container->get('entity.memory_cache')->reset();
}
}
diff --git a/core/lib/Drupal/Core/Entity/EntityTypeManagerInterface.php b/core/lib/Drupal/Core/Entity/EntityTypeManagerInterface.php
index d395be2..0ccac97 100644
--- a/core/lib/Drupal/Core/Entity/EntityTypeManagerInterface.php
+++ b/core/lib/Drupal/Core/Entity/EntityTypeManagerInterface.php
@@ -30,7 +30,10 @@ interface EntityTypeManagerInterface extends PluginManagerInterface, CachedDisco
* @return \Drupal\Core\Entity\EntityStorageInterface
* A storage instance.
*
+ * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
+ * Thrown if the entity type doesn't exist.
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
+ * Thrown if the storage handler couldn't be loaded.
*/
public function getStorage($entity_type);
diff --git a/core/lib/Drupal/Core/Entity/EntityViewBuilder.php b/core/lib/Drupal/Core/Entity/EntityViewBuilder.php
index cd0e06a..da1a43b 100644
--- a/core/lib/Drupal/Core/Entity/EntityViewBuilder.php
+++ b/core/lib/Drupal/Core/Entity/EntityViewBuilder.php
@@ -11,7 +11,7 @@ use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Theme\Registry;
-use Drupal\Core\TypedData\TranslatableInterface;
+use Drupal\Core\TypedData\TranslatableInterface as TranslatableDataInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
@@ -52,7 +52,7 @@ class EntityViewBuilder extends EntityHandlerBase implements EntityHandlerInterf
/**
* The language manager.
*
- * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
+ * @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
@@ -66,9 +66,9 @@ class EntityViewBuilder extends EntityHandlerBase implements EntityHandlerInterf
/**
* The EntityViewDisplay objects created for individual field rendering.
*
- * @see \Drupal\Core\Entity\EntityViewBuilder::getSingleFieldDisplay()
+ * @var \Drupal\Core\Entity\Display\EntityViewDisplayInterface[]
*
- * @param \Drupal\Core\Entity\Display\EntityViewDisplayInterface[]
+ * @see \Drupal\Core\Entity\EntityViewBuilder::getSingleFieldDisplay()
*/
protected $singleFieldDisplays;
@@ -189,7 +189,7 @@ class EntityViewBuilder extends EntityHandlerBase implements EntityHandlerInterf
'bin' => $this->cacheBin,
];
- if ($entity instanceof TranslatableInterface && count($entity->getTranslationLanguages()) > 1) {
+ if ($entity instanceof TranslatableDataInterface && count($entity->getTranslationLanguages()) > 1) {
$build['#cache']['keys'][] = $entity->language()->getId();
}
}
@@ -213,7 +213,7 @@ class EntityViewBuilder extends EntityHandlerBase implements EntityHandlerInterf
* @return array
* The updated renderable array.
*
- * @see drupal_render()
+ * @see \Drupal\Core\Render\RendererInterface::render()
*/
public function build(array $build) {
$build_list = [$build];
@@ -237,7 +237,7 @@ class EntityViewBuilder extends EntityHandlerBase implements EntityHandlerInterf
* @return array
* The updated renderable array.
*
- * @see drupal_render()
+ * @see \Drupal\Core\Render\RendererInterface::render()
*/
public function buildMultiple(array $build_list) {
// Build the view modes and display objects.
@@ -269,6 +269,7 @@ class EntityViewBuilder extends EntityHandlerBase implements EntityHandlerInterf
$this->moduleHandler()->invokeAll($view_hook, [&$build_list[$key], $entity, $display, $view_mode]);
$this->moduleHandler()->invokeAll('entity_view', [&$build_list[$key], $entity, $display, $view_mode]);
+ $this->addContextualLinks($build_list[$key], $entity);
$this->alterBuild($build_list[$key], $entity, $display, $view_mode);
// Assign the weights configured in the display.
@@ -325,6 +326,36 @@ class EntityViewBuilder extends EntityHandlerBase implements EntityHandlerInterf
}
/**
+ * Add contextual links.
+ *
+ * @param array $build
+ * The render array that is being created.
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The entity to be prepared.
+ */
+ protected function addContextualLinks(array &$build, EntityInterface $entity) {
+ if ($entity->isNew()) {
+ return;
+ }
+ $key = $entity->getEntityTypeId();
+ $rel = 'canonical';
+ if ($entity instanceof ContentEntityInterface && !$entity->isDefaultRevision()) {
+ $rel = 'revision';
+ $key .= '_revision';
+ }
+ if ($entity->hasLinkTemplate($rel)) {
+ $build['#contextual_links'][$key] = [
+ 'route_parameters' => $entity->toUrl($rel)->getRouteParameters(),
+ ];
+ if ($entity instanceof EntityChangedInterface) {
+ $build['#contextual_links'][$key]['metadata'] = [
+ 'changed' => $entity->getChangedTime(),
+ ];
+ }
+ }
+ }
+
+ /**
* Specific per-entity building.
*
* @param array $build
diff --git a/core/lib/Drupal/Core/Entity/FieldableEntityStorageInterface.php b/core/lib/Drupal/Core/Entity/FieldableEntityStorageInterface.php
index 4d91b29..79c57f9 100644
--- a/core/lib/Drupal/Core/Entity/FieldableEntityStorageInterface.php
+++ b/core/lib/Drupal/Core/Entity/FieldableEntityStorageInterface.php
@@ -2,6 +2,9 @@
namespace Drupal\Core\Entity;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+
/**
* A storage that supports entity types with field definitions.
*/
@@ -24,4 +27,27 @@ interface FieldableEntityStorageInterface extends EntityStorageInterface {
*/
public function countFieldData($storage_definition, $as_bool = FALSE);
+ /**
+ * Purges a batch of field data.
+ *
+ * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
+ * The deleted field whose data is being purged.
+ * @param int $batch_size
+ * The maximum number of field data records to purge before returning,
+ * relating to the count of field data records returned by
+ * \Drupal\Core\Entity\FieldableEntityStorageInterface::countFieldData().
+ *
+ * @return int
+ * The number of field data records that have been purged.
+ */
+ public function purgeFieldData(FieldDefinitionInterface $field_definition, $batch_size);
+
+ /**
+ * Performs final cleanup after all data of a field has been purged.
+ *
+ * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+ * The field storage being purged.
+ */
+ public function finalizePurge(FieldStorageDefinitionInterface $storage_definition);
+
}
diff --git a/core/lib/Drupal/Core/Entity/Form/DeleteMultipleForm.php b/core/lib/Drupal/Core/Entity/Form/DeleteMultipleForm.php
new file mode 100644
index 0000000..aea3291
--- /dev/null
+++ b/core/lib/Drupal/Core/Entity/Form/DeleteMultipleForm.php
@@ -0,0 +1,323 @@
+<?php
+
+namespace Drupal\Core\Entity\Form;
+
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Form\BaseFormIdInterface;
+use Drupal\Core\Form\ConfirmFormBase;
+use Drupal\Core\Messenger\MessengerInterface;
+use Drupal\Core\TypedData\TranslatableInterface;
+use Drupal\Core\Url;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\TempStore\PrivateTempStoreFactory;
+use Symfony\Component\HttpFoundation\RedirectResponse;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides an entities deletion confirmation form.
+ */
+class DeleteMultipleForm extends ConfirmFormBase implements BaseFormIdInterface {
+
+ /**
+ * The current user.
+ *
+ * @var \Drupal\Core\Session\AccountInterface
+ */
+ protected $currentUser;
+
+ /**
+ * The entity type manager.
+ *
+ * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+ */
+ protected $entityTypeManager;
+
+ /**
+ * The tempstore.
+ *
+ * @var \Drupal\Core\TempStore\SharedTempStore
+ */
+ protected $tempStore;
+
+ /**
+ * The messenger service.
+ *
+ * @var \Drupal\Core\Messenger\MessengerInterface
+ */
+ protected $messenger;
+
+ /**
+ * The entity type ID.
+ *
+ * @var string
+ */
+ protected $entityTypeId;
+
+ /**
+ * The selection, in the entity_id => langcodes format.
+ *
+ * @var array
+ */
+ protected $selection = [];
+
+ /**
+ * The entity type definition.
+ *
+ * @var \Drupal\Core\Entity\EntityTypeInterface
+ */
+ protected $entityType;
+
+ /**
+ * Constructs a new DeleteMultiple object.
+ *
+ * @param \Drupal\Core\Session\AccountInterface $current_user
+ * The current user.
+ * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+ * The entity type manager.
+ * @param \Drupal\Core\TempStore\PrivateTempStoreFactory $temp_store_factory
+ * The tempstore factory.
+ * @param \Drupal\Core\Messenger\MessengerInterface $messenger
+ * The messenger service.
+ */
+ public function __construct(AccountInterface $current_user, EntityTypeManagerInterface $entity_type_manager, PrivateTempStoreFactory $temp_store_factory, MessengerInterface $messenger) {
+ $this->currentUser = $current_user;
+ $this->entityTypeManager = $entity_type_manager;
+ $this->tempStore = $temp_store_factory->get('entity_delete_multiple_confirm');
+ $this->messenger = $messenger;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static(
+ $container->get('current_user'),
+ $container->get('entity_type.manager'),
+ $container->get('tempstore.private'),
+ $container->get('messenger')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getBaseFormId() {
+ return 'entity_delete_multiple_confirm_form';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFormId() {
+ // Get entity type ID from the route because ::buildForm has not yet been
+ // called.
+ $entity_type_id = $this->getRouteMatch()->getParameter('entity_type_id');
+ return $entity_type_id . '_delete_multiple_confirm_form';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getQuestion() {
+ return $this->formatPlural(count($this->selection), 'Are you sure you want to delete this @item?', 'Are you sure you want to delete these @items?', [
+ '@item' => $this->entityType->getSingularLabel(),
+ '@items' => $this->entityType->getPluralLabel(),
+ ]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCancelUrl() {
+ if ($this->entityType->hasLinkTemplate('collection')) {
+ return new Url('entity.' . $this->entityTypeId . '.collection');
+ }
+ else {
+ return new Url('<front>');
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getConfirmText() {
+ return $this->t('Delete');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(array $form, FormStateInterface $form_state, $entity_type_id = NULL) {
+ $this->entityTypeId = $entity_type_id;
+ $this->entityType = $this->entityTypeManager->getDefinition($this->entityTypeId);
+ $this->selection = $this->tempStore->get($this->currentUser->id() . ':' . $entity_type_id);
+ if (empty($this->entityTypeId) || empty($this->selection)) {
+ return new RedirectResponse($this->getCancelUrl()
+ ->setAbsolute()
+ ->toString());
+ }
+
+ $items = [];
+ $entities = $this->entityTypeManager->getStorage($entity_type_id)->loadMultiple(array_keys($this->selection));
+ foreach ($this->selection as $id => $selected_langcodes) {
+ $entity = $entities[$id];
+ foreach ($selected_langcodes as $langcode) {
+ $key = $id . ':' . $langcode;
+ if ($entity instanceof TranslatableInterface) {
+ $entity = $entity->getTranslation($langcode);
+ $default_key = $id . ':' . $entity->getUntranslated()->language()->getId();
+
+ // Build a nested list of translations that will be deleted if the
+ // entity has multiple translations.
+ $entity_languages = $entity->getTranslationLanguages();
+ if (count($entity_languages) > 1 && $entity->isDefaultTranslation()) {
+ $names = [];
+ foreach ($entity_languages as $translation_langcode => $language) {
+ $names[] = $language->getName();
+ unset($items[$id . ':' . $translation_langcode]);
+ }
+ $items[$default_key] = [
+ 'label' => [
+ '#markup' => $this->t('@label (Original translation) - <em>The following @entity_type translations will be deleted:</em>',
+ [
+ '@label' => $entity->label(),
+ '@entity_type' => $this->entityType->getSingularLabel(),
+ ]),
+ ],
+ 'deleted_translations' => [
+ '#theme' => 'item_list',
+ '#items' => $names,
+ ],
+ ];
+ }
+ elseif (!isset($items[$default_key])) {
+ $items[$key] = $entity->label();
+ }
+ }
+ elseif (!isset($items[$key])) {
+ $items[$key] = $entity->label();
+ }
+ }
+ }
+
+ $form['entities'] = [
+ '#theme' => 'item_list',
+ '#items' => $items,
+ ];
+ $form = parent::buildForm($form, $form_state);
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ $total_count = 0;
+ $delete_entities = [];
+ $delete_translations = [];
+ $inaccessible_entities = [];
+ $storage = $this->entityTypeManager->getStorage($this->entityTypeId);
+
+ $entities = $storage->loadMultiple(array_keys($this->selection));
+ foreach ($this->selection as $id => $selected_langcodes) {
+ $entity = $entities[$id];
+ if (!$entity->access('delete', $this->currentUser)) {
+ $inaccessible_entities[] = $entity;
+ continue;
+ }
+ foreach ($selected_langcodes as $langcode) {
+ if ($entity instanceof TranslatableInterface) {
+ $entity = $entity->getTranslation($langcode);
+ // If the entity is the default translation then deleting it will
+ // delete all the translations.
+ if ($entity->isDefaultTranslation()) {
+ $delete_entities[$id] = $entity;
+ // If there are translations already marked for deletion then remove
+ // them as they will be deleted anyway.
+ unset($delete_translations[$id]);
+ // Update the total count. Since a single delete will delete all
+ // translations, we need to add the number of translations to the
+ // count.
+ $total_count += count($entity->getTranslationLanguages());
+ }
+ // Add the translation to the list of translations to be deleted
+ // unless the default translation is being deleted.
+ elseif (!isset($delete_entities[$id])) {
+ $delete_translations[$id][] = $entity;
+ }
+ }
+ elseif (!isset($delete_entities[$id])) {
+ $delete_entities[$id] = $entity;
+ $total_count++;
+ }
+ }
+ }
+
+ if ($delete_entities) {
+ $storage->delete($delete_entities);
+ foreach ($delete_entities as $entity) {
+ $this->logger($entity->getEntityType()->getProvider())->notice('The @entity-type %label has been deleted.', [
+ '@entity-type' => $entity->getEntityType()->getLowercaseLabel(),
+ '%label' => $entity->label(),
+ ]);
+ }
+ }
+
+ if ($delete_translations) {
+ /** @var \Drupal\Core\Entity\TranslatableInterface[][] $delete_translations */
+ foreach ($delete_translations as $id => $translations) {
+ $entity = $entities[$id]->getUntranslated();
+ foreach ($translations as $translation) {
+ $entity->removeTranslation($translation->language()->getId());
+ }
+ $entity->save();
+ foreach ($translations as $translation) {
+ $this->logger($entity->getEntityType()->getProvider())->notice('The @entity-type %label @language translation has been deleted.', [
+ '@entity-type' => $entity->getEntityType()->getLowercaseLabel(),
+ '%label' => $entity->label(),
+ '@language' => $translation->language()->getName(),
+ ]);
+ }
+ $total_count += count($translations);
+ }
+ }
+
+ if ($total_count) {
+ $this->messenger->addStatus($this->getDeletedMessage($total_count));
+ }
+ if ($inaccessible_entities) {
+ $this->messenger->addWarning($this->getInaccessibleMessage(count($inaccessible_entities)));
+ }
+ $this->tempStore->delete($this->currentUser->id());
+ $form_state->setRedirectUrl($this->getCancelUrl());
+ }
+
+ /**
+ * Returns the message to show the user after an item was deleted.
+ *
+ * @param int $count
+ * Count of deleted translations.
+ *
+ * @return \Drupal\Core\StringTranslation\TranslatableMarkup
+ * The item deleted message.
+ */
+ protected function getDeletedMessage($count) {
+ return $this->formatPlural($count, 'Deleted @count item.', 'Deleted @count items.');
+ }
+
+ /**
+ * Returns the message to show the user when an item has not been deleted.
+ *
+ * @param int $count
+ * Count of deleted translations.
+ *
+ * @return \Drupal\Core\StringTranslation\TranslatableMarkup
+ * The item inaccessible message.
+ */
+ protected function getInaccessibleMessage($count) {
+ return $this->formatPlural($count, "@count item has not been deleted because you do not have the necessary permissions.", "@count items have not been deleted because you do not have the necessary permissions.");
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Entity/HtmlEntityFormController.php b/core/lib/Drupal/Core/Entity/HtmlEntityFormController.php
index 73e828f..586938e 100644
--- a/core/lib/Drupal/Core/Entity/HtmlEntityFormController.php
+++ b/core/lib/Drupal/Core/Entity/HtmlEntityFormController.php
@@ -2,10 +2,10 @@
namespace Drupal\Core\Entity;
-use Drupal\Core\Controller\ControllerResolverInterface;
use Drupal\Core\Controller\FormController;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Routing\RouteMatchInterface;
+use Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface;
/**
* Wrapping controller for entity forms that serve as the main page body.
@@ -22,15 +22,15 @@ class HtmlEntityFormController extends FormController {
/**
* Constructs a new \Drupal\Core\Routing\Enhancer\FormEnhancer object.
*
- * @param \Drupal\Core\Controller\ControllerResolverInterface $resolver
- * The controller resolver.
+ * @param \Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface $argument_resolver
+ * The argument resolver.
* @param \Drupal\Core\Form\FormBuilderInterface $form_builder
* The form builder.
* @param \Drupal\Core\Entity\EntityManagerInterface $manager
* The entity manager.
*/
- public function __construct(ControllerResolverInterface $resolver, FormBuilderInterface $form_builder, EntityManagerInterface $manager) {
- parent::__construct($resolver, $form_builder);
+ public function __construct(ArgumentResolverInterface $argument_resolver, FormBuilderInterface $form_builder, EntityManagerInterface $manager) {
+ parent::__construct($argument_resolver, $form_builder);
$this->entityManager = $manager;
}
diff --git a/core/lib/Drupal/Core/Entity/KeyValueStore/KeyValueContentEntityStorage.php b/core/lib/Drupal/Core/Entity/KeyValueStore/KeyValueContentEntityStorage.php
index b3fc12d..6aa18ff 100644
--- a/core/lib/Drupal/Core/Entity/KeyValueStore/KeyValueContentEntityStorage.php
+++ b/core/lib/Drupal/Core/Entity/KeyValueStore/KeyValueContentEntityStorage.php
@@ -4,6 +4,8 @@ namespace Drupal\Core\Entity\KeyValueStore;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\ContentEntityStorageInterface;
+use Drupal\Core\Entity\RevisionableInterface;
+use Drupal\Core\Entity\TranslatableInterface;
/**
* Provides a key value backend for content entities.
@@ -21,6 +23,41 @@ class KeyValueContentEntityStorage extends KeyValueEntityStorage implements Cont
/**
* {@inheritdoc}
*/
+ public function hasStoredTranslations(TranslatableInterface $entity) {
+ return FALSE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createRevision(RevisionableInterface $entity, $default = TRUE, $keep_untranslatable_fields = NULL) {
+ return NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
public function createWithSampleValues($bundle = FALSE, array $values = []) {}
+ /**
+ * {@inheritdoc}
+ */
+ public function loadMultipleRevisions(array $revision_ids) {
+ return [];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getLatestRevisionId($entity_id) {
+ return NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getLatestTranslationAffectedRevisionId($entity_id, $langcode) {
+ return NULL;
+ }
+
}
diff --git a/core/lib/Drupal/Core/Entity/KeyValueStore/KeyValueEntityStorage.php b/core/lib/Drupal/Core/Entity/KeyValueStore/KeyValueEntityStorage.php
index cd2f26e..113f630 100644
--- a/core/lib/Drupal/Core/Entity/KeyValueStore/KeyValueEntityStorage.php
+++ b/core/lib/Drupal/Core/Entity/KeyValueStore/KeyValueEntityStorage.php
@@ -3,6 +3,7 @@
namespace Drupal\Core\Entity\KeyValueStore;
use Drupal\Component\Uuid\UuidInterface;
+use Drupal\Core\Cache\MemoryCache\MemoryCacheInterface;
use Drupal\Core\Config\Entity\Exception\ConfigEntityIdLengthException;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Entity\EntityInterface;
@@ -60,9 +61,11 @@ class KeyValueEntityStorage extends EntityStorageBase {
* The UUID service.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
+ * @param \Drupal\Core\Cache\MemoryCache\MemoryCacheInterface $memory_cache
+ * The memory cache.
*/
- public function __construct(EntityTypeInterface $entity_type, KeyValueStoreInterface $key_value_store, UuidInterface $uuid_service, LanguageManagerInterface $language_manager) {
- parent::__construct($entity_type);
+ public function __construct(EntityTypeInterface $entity_type, KeyValueStoreInterface $key_value_store, UuidInterface $uuid_service, LanguageManagerInterface $language_manager, MemoryCacheInterface $memory_cache = NULL) {
+ parent::__construct($entity_type, $memory_cache);
$this->keyValueStore = $key_value_store;
$this->uuidService = $uuid_service;
$this->languageManager = $language_manager;
@@ -79,7 +82,8 @@ class KeyValueEntityStorage extends EntityStorageBase {
$entity_type,
$container->get('keyvalue')->get('entity_storage__' . $entity_type->id()),
$container->get('uuid'),
- $container->get('language_manager')
+ $container->get('language_manager'),
+ $container->get('entity.memory_cache')
);
}
diff --git a/core/lib/Drupal/Core/Entity/Plugin/DataType/ConfigEntityAdapter.php b/core/lib/Drupal/Core/Entity/Plugin/DataType/ConfigEntityAdapter.php
new file mode 100644
index 0000000..55a8bba
--- /dev/null
+++ b/core/lib/Drupal/Core/Entity/Plugin/DataType/ConfigEntityAdapter.php
@@ -0,0 +1,146 @@
+<?php
+
+namespace Drupal\Core\Entity\Plugin\DataType;
+
+use Drupal\Core\Config\TypedConfigManagerInterface;
+use Drupal\Core\TypedData\Exception\MissingDataException;
+use Drupal\Core\TypedData\TypedDataManagerInterface;
+
+/**
+ * Enhances EntityAdapter for config entities.
+ */
+class ConfigEntityAdapter extends EntityAdapter {
+
+ /**
+ * The wrapped entity object.
+ *
+ * @var \Drupal\Core\Config\Entity\ConfigEntityInterface
+ */
+ protected $entity;
+
+ /**
+ * The typed config manager.
+ *
+ * @var \Drupal\Core\Config\TypedConfigManagerInterface
+ */
+ protected $typedConfigManager;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function get($property_name) {
+ if (!isset($this->entity)) {
+ throw new MissingDataException("Unable to get property $property_name as no entity has been provided.");
+ }
+ return $this->getConfigTypedData()->get($property_name);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function set($property_name, $value, $notify = TRUE) {
+ if (!isset($this->entity)) {
+ throw new MissingDataException("Unable to set property $property_name as no entity has been provided.");
+ }
+ $this->entity->set($property_name, $value, $notify);
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getProperties($include_computed = FALSE) {
+ if (!isset($this->entity)) {
+ throw new MissingDataException('Unable to get properties as no entity has been provided.');
+ }
+ return $this->getConfigTypedData()->getProperties($include_computed);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function onChange($property_name) {
+ if (isset($this->entity)) {
+ // Let the entity know of any changes.
+ $this->getConfigTypedData()->onChange($property_name);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getIterator() {
+ if (isset($this->entity)) {
+ return $this->getConfigTypedData()->getIterator();
+ }
+ return new \ArrayIterator([]);
+ }
+
+ /**
+ * Gets the typed config manager.
+ *
+ * @return \Drupal\Core\Config\TypedConfigManagerInterface
+ * The typed config manager.
+ */
+ protected function getTypedConfigManager() {
+ if (empty($this->typedConfigManager)) {
+ // Use the typed data manager if it is also the typed config manager.
+ // @todo Remove this in https://www.drupal.org/node/3011137.
+ $typed_data_manager = $this->getTypedDataManager();
+ if ($typed_data_manager instanceof TypedConfigManagerInterface) {
+ $this->typedConfigManager = $typed_data_manager;
+ }
+ else {
+ $this->typedConfigManager = \Drupal::service('config.typed');
+ }
+ }
+
+ return $this->typedConfigManager;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @todo Remove this in https://www.drupal.org/node/3011137.
+ */
+ public function getTypedDataManager() {
+ if (empty($this->typedDataManager)) {
+ $this->typedDataManager = \Drupal::service('config.typed');
+ }
+
+ return $this->typedDataManager;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @todo Remove this in https://www.drupal.org/node/3011137.
+ */
+ public function setTypedDataManager(TypedDataManagerInterface $typed_data_manager) {
+ $this->typedDataManager = $typed_data_manager;
+ if ($typed_data_manager instanceof TypedConfigManagerInterface) {
+ $this->typedConfigManager = $typed_data_manager;
+ }
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function applyDefaultValue($notify = TRUE) {
+ // @todo Figure out what to do for this method, see
+ // https://www.drupal.org/project/drupal/issues/2945635.
+ throw new \BadMethodCallException('Method not supported');
+ }
+
+ /**
+ * Gets typed data for config entity.
+ *
+ * @return \Drupal\Core\TypedData\ComplexDataInterface
+ * The typed data.
+ */
+ protected function getConfigTypedData() {
+ return $this->getTypedConfigManager()->createFromNameAndData($this->entity->getConfigDependencyName(), $this->entity->toArray());
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Entity/Plugin/DataType/Deriver/EntityDeriver.php b/core/lib/Drupal/Core/Entity/Plugin/DataType/Deriver/EntityDeriver.php
index 98be20e..38ac960 100644
--- a/core/lib/Drupal/Core/Entity/Plugin/DataType/Deriver/EntityDeriver.php
+++ b/core/lib/Drupal/Core/Entity/Plugin/DataType/Deriver/EntityDeriver.php
@@ -2,8 +2,11 @@
namespace Drupal\Core\Entity\Plugin\DataType\Deriver;
+use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
+use Drupal\Core\Entity\Plugin\DataType\ConfigEntityAdapter;
+use Drupal\Core\Entity\Plugin\DataType\EntityAdapter;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -86,17 +89,21 @@ class EntityDeriver implements ContainerDeriverInterface {
$this->derivatives[''] = $base_plugin_definition;
// Add definitions for each entity type and bundle.
foreach ($this->entityManager->getDefinitions() as $entity_type_id => $entity_type) {
+ $class = $entity_type->entityClassImplements(ConfigEntityInterface::class) ? ConfigEntityAdapter::class : EntityAdapter::class;
$this->derivatives[$entity_type_id] = [
+ 'class' => $class,
'label' => $entity_type->getLabel(),
'constraints' => $entity_type->getConstraints(),
+ 'internal' => $entity_type->isInternal(),
] + $base_plugin_definition;
// Incorporate the bundles as entity:$entity_type:$bundle, if any.
foreach ($this->bundleInfoService->getBundleInfo($entity_type_id) as $bundle => $bundle_info) {
if ($bundle !== $entity_type_id) {
$this->derivatives[$entity_type_id . ':' . $bundle] = [
+ 'class' => $class,
'label' => $bundle_info['label'],
- 'constraints' => $this->derivatives[$entity_type_id]['constraints']
+ 'constraints' => $this->derivatives[$entity_type_id]['constraints'],
] + $base_plugin_definition;
}
}
diff --git a/core/lib/Drupal/Core/Entity/Plugin/DataType/EntityAdapter.php b/core/lib/Drupal/Core/Entity/Plugin/DataType/EntityAdapter.php
index db194b5..1b54d70 100644
--- a/core/lib/Drupal/Core/Entity/Plugin/DataType/EntityAdapter.php
+++ b/core/lib/Drupal/Core/Entity/Plugin/DataType/EntityAdapter.php
@@ -78,8 +78,6 @@ class EntityAdapter extends TypedData implements \IteratorAggregate, ComplexData
throw new MissingDataException("Unable to get property $property_name as no entity has been provided.");
}
if (!$this->entity instanceof FieldableEntityInterface) {
- // @todo: Add support for config entities in
- // https://www.drupal.org/node/1818574.
throw new \InvalidArgumentException("Unable to get unknown property $property_name.");
}
// This will throw an exception for unknown fields.
@@ -94,8 +92,6 @@ class EntityAdapter extends TypedData implements \IteratorAggregate, ComplexData
throw new MissingDataException("Unable to set property $property_name as no entity has been provided.");
}
if (!$this->entity instanceof FieldableEntityInterface) {
- // @todo: Add support for config entities in
- // https://www.drupal.org/node/1818574.
throw new \InvalidArgumentException("Unable to set unknown property $property_name.");
}
// This will throw an exception for unknown fields.
@@ -111,8 +107,6 @@ class EntityAdapter extends TypedData implements \IteratorAggregate, ComplexData
throw new MissingDataException('Unable to get properties as no entity has been provided.');
}
if (!$this->entity instanceof FieldableEntityInterface) {
- // @todo: Add support for config entities in
- // https://www.drupal.org/node/1818574.
return [];
}
return $this->entity->getFields($include_computed);
@@ -167,7 +161,7 @@ class EntityAdapter extends TypedData implements \IteratorAggregate, ComplexData
* {@inheritdoc}
*/
public function getIterator() {
- return isset($this->entity) ? $this->entity->getIterator() : new \ArrayIterator([]);
+ return $this->entity instanceof \IteratorAggregate ? $this->entity->getIterator() : new \ArrayIterator([]);
}
}
diff --git a/core/lib/Drupal/Core/Entity/Plugin/DataType/EntityReference.php b/core/lib/Drupal/Core/Entity/Plugin/DataType/EntityReference.php
index 6cf57eb..a07e63d 100644
--- a/core/lib/Drupal/Core/Entity/Plugin/DataType/EntityReference.php
+++ b/core/lib/Drupal/Core/Entity/Plugin/DataType/EntityReference.php
@@ -16,7 +16,7 @@ use Drupal\Core\TypedData\DataReferenceBase;
* or the entity ID may be passed.
*
* Note that the definition of the referenced entity's type is required, whereas
- * defining referencable entity bundle(s) is optional. A reference defining the
+ * defining referenceable entity bundle(s) is optional. A reference defining the
* type and bundle of the referenced entity can be created as following:
* @code
* $definition = \Drupal\Core\Entity\EntityDefinition::create($entity_type)
diff --git a/core/lib/Drupal/Core/Entity/Plugin/EntityReferenceSelection/DefaultSelection.php b/core/lib/Drupal/Core/Entity/Plugin/EntityReferenceSelection/DefaultSelection.php
index 218131c..2764866 100644
--- a/core/lib/Drupal/Core/Entity/Plugin/EntityReferenceSelection/DefaultSelection.php
+++ b/core/lib/Drupal/Core/Entity/Plugin/EntityReferenceSelection/DefaultSelection.php
@@ -91,7 +91,7 @@ class DefaultSelection extends SelectionPluginBase implements ContainerFactoryPl
$form['target_bundles'] = [
'#type' => 'checkboxes',
- '#title' => $this->t('Bundles'),
+ '#title' => $entity_type->getBundleLabel(),
'#options' => $bundle_options,
'#default_value' => (array) $configuration['target_bundles'],
'#required' => TRUE,
diff --git a/core/lib/Drupal/Core/Entity/Plugin/EntityReferenceSelection/PhpSelection.php b/core/lib/Drupal/Core/Entity/Plugin/EntityReferenceSelection/PhpSelection.php
index 4545ebb..52cf33d 100644
--- a/core/lib/Drupal/Core/Entity/Plugin/EntityReferenceSelection/PhpSelection.php
+++ b/core/lib/Drupal/Core/Entity/Plugin/EntityReferenceSelection/PhpSelection.php
@@ -3,7 +3,6 @@
namespace Drupal\Core\Entity\Plugin\EntityReferenceSelection;
use Drupal\Component\Utility\Html;
-use Drupal\Component\Utility\Unicode;
/**
* Defines an alternative to the default Entity Reference Selection plugin.
@@ -35,11 +34,11 @@ class PhpSelection extends DefaultSelection {
// possible.
// @see \Drupal\Core\Entity\EntityReferenceSelection\SelectionInterface::getReferenceableEntities()
if (is_string($match)) {
- $match = Html::escape(Unicode::strtolower($match));
+ $match = Html::escape(mb_strtolower($match));
}
elseif (is_array($match)) {
array_walk($match, function (&$item) {
- $item = Html::escape(Unicode::strtolower($item));
+ $item = Html::escape(mb_strtolower($item));
});
}
@@ -89,7 +88,7 @@ class PhpSelection extends DefaultSelection {
*/
protected function matchLabel($match, $match_operator, $label) {
// Always use a case-insensitive value.
- $label = Unicode::strtolower($label);
+ $label = mb_strtolower($label);
switch ($match_operator) {
case '=':
@@ -113,7 +112,7 @@ class PhpSelection extends DefaultSelection {
case 'CONTAINS':
return strpos($label, $match) !== FALSE;
case 'ENDS_WITH':
- return Unicode::substr($label, -Unicode::strlen($match)) === (string) $match;
+ return mb_substr($label, -mb_strlen($match)) === (string) $match;
case 'IS NOT NULL':
return TRUE;
case 'IS NULL':
diff --git a/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityChangedConstraintValidator.php b/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityChangedConstraintValidator.php
index b1fee2a..28d81ba 100644
--- a/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityChangedConstraintValidator.php
+++ b/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityChangedConstraintValidator.php
@@ -18,10 +18,23 @@ class EntityChangedConstraintValidator extends ConstraintValidator {
/** @var \Drupal\Core\Entity\EntityInterface $entity */
if (!$entity->isNew()) {
$saved_entity = \Drupal::entityManager()->getStorage($entity->getEntityTypeId())->loadUnchanged($entity->id());
- // A change to any other translation must add a violation to the current
- // translation because there might be untranslatable shared fields.
- if ($saved_entity && $saved_entity->getChangedTimeAcrossTranslations() > $entity->getChangedTimeAcrossTranslations()) {
- $this->context->addViolation($constraint->message);
+ // Ensure that all the entity translations are the same as or newer
+ // than their current version in the storage in order to avoid
+ // reverting other changes. In fact the entity object that is being
+ // saved might contain an older entity translation when different
+ // translations are being concurrently edited.
+ if ($saved_entity) {
+ $common_translation_languages = array_intersect_key($entity->getTranslationLanguages(), $saved_entity->getTranslationLanguages());
+ foreach (array_keys($common_translation_languages) as $langcode) {
+ // Merely comparing the latest changed timestamps across all
+ // translations is not sufficient since other translations may have
+ // been edited and saved in the meanwhile. Therefore, compare the
+ // changed timestamps of each entity translation individually.
+ if ($saved_entity->getTranslation($langcode)->getChangedTime() > $entity->getTranslation($langcode)->getChangedTime()) {
+ $this->context->addViolation($constraint->message);
+ break;
+ }
+ }
}
}
}
diff --git a/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityHasFieldConstraint.php b/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityHasFieldConstraint.php
new file mode 100644
index 0000000..6a721ef
--- /dev/null
+++ b/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityHasFieldConstraint.php
@@ -0,0 +1,53 @@
+<?php
+
+namespace Drupal\Core\Entity\Plugin\Validation\Constraint;
+
+use Symfony\Component\Validator\Constraint;
+
+/**
+ * Checks if a value is an entity that has a specific field.
+ *
+ * @Constraint(
+ * id = "EntityHasField",
+ * label = @Translation("Entity has field", context = "Validation"),
+ * type = { "entity" },
+ * )
+ */
+class EntityHasFieldConstraint extends Constraint {
+
+ /**
+ * The default violation message.
+ *
+ * @var string
+ */
+ public $message = 'The entity must have the %field_name field.';
+
+ /**
+ * The violation message for non-fieldable entities.
+ *
+ * @var string
+ */
+ public $notFieldableMessage = 'The entity does not support fields.';
+
+ /**
+ * The field name option.
+ *
+ * @var string
+ */
+ public $field_name;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDefaultOption() {
+ return 'field_name';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getRequiredOptions() {
+ return (array) $this->getDefaultOption();
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityHasFieldConstraintValidator.php b/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityHasFieldConstraintValidator.php
new file mode 100644
index 0000000..d16a079
--- /dev/null
+++ b/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityHasFieldConstraintValidator.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace Drupal\Core\Entity\Plugin\Validation\Constraint;
+
+use Drupal\Core\Entity\FieldableEntityInterface;
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\ConstraintValidator;
+
+/**
+ * Validates the EntityHasField constraint.
+ */
+class EntityHasFieldConstraintValidator extends ConstraintValidator {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validate($entity, Constraint $constraint) {
+ if (!isset($entity)) {
+ return;
+ }
+
+ /** @var \Drupal\Core\Entity\Plugin\Validation\Constraint\EntityHasFieldConstraint $constraint */
+ if (!($entity instanceof FieldableEntityInterface)) {
+ $this->context->addViolation($constraint->notFieldableMessage);
+ return;
+ }
+
+ if (!$entity->hasField($constraint->field_name)) {
+ $this->context->addViolation($constraint->message, [
+ '%field_name' => $constraint->field_name,
+ ]);
+ }
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityUntranslatableFieldsConstraint.php b/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityUntranslatableFieldsConstraint.php
new file mode 100644
index 0000000..bb1a924
--- /dev/null
+++ b/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityUntranslatableFieldsConstraint.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace Drupal\Core\Entity\Plugin\Validation\Constraint;
+
+use Symfony\Component\Validator\Constraint;
+
+/**
+ * Validation constraint for the entity changed timestamp.
+ *
+ * @Constraint(
+ * id = "EntityUntranslatableFields",
+ * label = @Translation("Entity untranslatable fields", context = "Validation"),
+ * type = {"entity"}
+ * )
+ */
+class EntityUntranslatableFieldsConstraint extends Constraint {
+
+ public $defaultRevisionMessage = 'Non-translatable fields can only be changed when updating the current revision.';
+ public $defaultTranslationMessage = 'Non-translatable fields can only be changed when updating the original language.';
+
+}
diff --git a/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityUntranslatableFieldsConstraintValidator.php b/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityUntranslatableFieldsConstraintValidator.php
new file mode 100644
index 0000000..730e539
--- /dev/null
+++ b/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityUntranslatableFieldsConstraintValidator.php
@@ -0,0 +1,123 @@
+<?php
+
+namespace Drupal\Core\Entity\Plugin\Validation\Constraint;
+
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Entity\EntityChangesDetectionTrait;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\ConstraintValidator;
+
+/**
+ * Validates the EntityChanged constraint.
+ */
+class EntityUntranslatableFieldsConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {
+
+ use EntityChangesDetectionTrait;
+
+ /**
+ * The entity type manager.
+ *
+ * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+ */
+ protected $entityTypeManager;
+
+ /**
+ * Constructs an EntityUntranslatableFieldsConstraintValidator object.
+ *
+ * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+ * The entity type manager.
+ */
+ public function __construct(EntityTypeManagerInterface $entity_type_manager) {
+ $this->entityTypeManager = $entity_type_manager;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static(
+ $container->get('entity_type.manager')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validate($entity, Constraint $constraint) {
+ /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
+ /** @var \Drupal\Core\Entity\Plugin\Validation\Constraint\EntityUntranslatableFieldsConstraint $constraint */
+
+ // Untranslatable field restrictions apply only to revisions of multilingual
+ // entities.
+ if ($entity->isNew() || !$entity->isTranslatable() || !$entity->getEntityType()->isRevisionable()) {
+ return;
+ }
+ if ($entity->isDefaultRevision() && !$entity->isDefaultTranslationAffectedOnly()) {
+ return;
+ }
+
+ // To avoid unintentional reverts and data losses, we forbid changes to
+ // untranslatable fields in pending revisions for multilingual entities. The
+ // only case where changes in pending revisions are acceptable is when
+ // untranslatable fields affect only the default translation, in which case
+ // a pending revision contains only one affected translation. Even in this
+ // case, multiple translations would be affected in a single revision, if we
+ // allowed changes to untranslatable fields while editing non-default
+ // translations, so that is forbidden too. For the same reason, when changes
+ // to untranslatable fields affect all translations, we can only allow them
+ // in default revisions.
+ if ($this->hasUntranslatableFieldsChanges($entity)) {
+ if ($entity->isDefaultTranslationAffectedOnly()) {
+ foreach ($entity->getTranslationLanguages(FALSE) as $langcode => $language) {
+ if ($entity->getTranslation($langcode)->hasTranslationChanges()) {
+ $this->context->addViolation($constraint->defaultTranslationMessage);
+ break;
+ }
+ }
+ }
+ else {
+ $this->context->addViolation($constraint->defaultRevisionMessage);
+ }
+ }
+ }
+
+ /**
+ * Checks whether an entity has untranslatable field changes.
+ *
+ * @param \Drupal\Core\Entity\ContentEntityInterface $entity
+ * A content entity object.
+ *
+ * @return bool
+ * TRUE if untranslatable fields have changes, FALSE otherwise.
+ */
+ protected function hasUntranslatableFieldsChanges(ContentEntityInterface $entity) {
+ $skip_fields = $this->getFieldsToSkipFromTranslationChangesCheck($entity);
+ /** @var \Drupal\Core\Entity\ContentEntityInterface $original */
+ if (isset($entity->original)) {
+ $original = $entity->original;
+ }
+ else {
+ $original = $this->entityTypeManager
+ ->getStorage($entity->getEntityTypeId())
+ ->loadRevision($entity->getLoadedRevisionId());
+ }
+
+ foreach ($entity->getFieldDefinitions() as $field_name => $definition) {
+ if (in_array($field_name, $skip_fields, TRUE) || $definition->isTranslatable() || $definition->isComputed()) {
+ continue;
+ }
+
+ $items = $entity->get($field_name)->filterEmptyItems();
+ $original_items = $original->get($field_name)->filterEmptyItems();
+ if ($items->hasAffectingChanges($original_items, $entity->getUntranslated()->language()->getId())) {
+ return TRUE;
+ }
+ }
+
+ return FALSE;
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Entity/Query/ConditionAggregateInterface.php b/core/lib/Drupal/Core/Entity/Query/ConditionAggregateInterface.php
index 875b8c2..383f41e 100644
--- a/core/lib/Drupal/Core/Entity/Query/ConditionAggregateInterface.php
+++ b/core/lib/Drupal/Core/Entity/Query/ConditionAggregateInterface.php
@@ -45,7 +45,7 @@ interface ConditionAggregateInterface extends \Countable {
*
* @param string $field
* @return ConditionInterface
- * @see \Drupal\Core\Entity\Query\QueryInterface::notexists()
+ * @see \Drupal\Core\Entity\Query\QueryInterface::notExists()
*/
public function notExists($field, $function, $langcode = NULL);
diff --git a/core/lib/Drupal/Core/Entity/Query/ConditionFundamentals.php b/core/lib/Drupal/Core/Entity/Query/ConditionFundamentals.php
index d03e2b2..6de10bf 100644
--- a/core/lib/Drupal/Core/Entity/Query/ConditionFundamentals.php
+++ b/core/lib/Drupal/Core/Entity/Query/ConditionFundamentals.php
@@ -65,7 +65,7 @@ abstract class ConditionFundamentals {
* {@inheritdoc}
*/
public function count() {
- return count($this->conditions) - 1;
+ return count($this->conditions);
}
/**
diff --git a/core/lib/Drupal/Core/Entity/Query/ConditionInterface.php b/core/lib/Drupal/Core/Entity/Query/ConditionInterface.php
index d42e48f..bf53a92 100644
--- a/core/lib/Drupal/Core/Entity/Query/ConditionInterface.php
+++ b/core/lib/Drupal/Core/Entity/Query/ConditionInterface.php
@@ -19,8 +19,7 @@ interface ConditionInterface {
* Implements \Countable::count().
*
* Returns the size of this conditional. The size of the conditional is the
- * size of its conditional array minus one, because one element is the
- * conjunction.
+ * size of its conditional array.
*/
public function count();
@@ -51,7 +50,7 @@ interface ConditionInterface {
*
* @param string $field
* @return ConditionInterface
- * @see \Drupal\Core\Entity\Query\QueryInterface::notexists()
+ * @see \Drupal\Core\Entity\Query\QueryInterface::notExists()
*/
public function notExists($field, $langcode = NULL);
diff --git a/core/lib/Drupal/Core/Entity/Query/QueryBase.php b/core/lib/Drupal/Core/Entity/Query/QueryBase.php
index d84a266..06dfb8f 100644
--- a/core/lib/Drupal/Core/Entity/Query/QueryBase.php
+++ b/core/lib/Drupal/Core/Entity/Query/QueryBase.php
@@ -449,7 +449,7 @@ abstract class QueryBase implements QueryInterface {
}
/**
- * Generates an alias for a field and it's aggregated function.
+ * Generates an alias for a field and its aggregated function.
*
* @param string $field
* The field name used in the alias.
diff --git a/core/lib/Drupal/Core/Entity/Query/QueryFactory.php b/core/lib/Drupal/Core/Entity/Query/QueryFactory.php
index a3c14b2..79e80cd 100644
--- a/core/lib/Drupal/Core/Entity/Query/QueryFactory.php
+++ b/core/lib/Drupal/Core/Entity/Query/QueryFactory.php
@@ -2,6 +2,8 @@
namespace Drupal\Core\Entity\Query;
+@trigger_error('The ' . __NAMESPACE__ . '\QueryFactory class is deprecated in Drupal 8.3.0, will be removed before Drupal 9.0.0. Use \Drupal\Core\Entity\EntityStorageInterface::getQuery() or \Drupal\Core\Entity\EntityStorageInterface::getAggregateQuery() instead. See https://www.drupal.org/node/2849874.', E_USER_DEPRECATED);
+
use Drupal\Core\Entity\EntityManagerInterface;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerAwareTrait;
@@ -12,11 +14,12 @@ use Symfony\Component\DependencyInjection\ContainerAwareTrait;
* Any implementation of this service must call getQuery()/getAggregateQuery()
* of the corresponding entity storage.
*
- * @see \Drupal\Core\Entity\EntityStorageBase::getQuery()
- *
* @deprecated in Drupal 8.3.0, will be removed before Drupal 9.0.0. Use
* \Drupal\Core\Entity\EntityStorageInterface::getQuery() or
* \Drupal\Core\Entity\EntityStorageInterface::getAggregateQuery() instead.
+ *
+ * @see https://www.drupal.org/node/2849874
+ * @see \Drupal\Core\Entity\EntityStorageBase::getQuery()
*/
class QueryFactory implements ContainerAwareInterface {
diff --git a/core/lib/Drupal/Core/Entity/Query/Sql/Condition.php b/core/lib/Drupal/Core/Entity/Query/Sql/Condition.php
index 00fe78c..7f93512 100644
--- a/core/lib/Drupal/Core/Entity/Query/Sql/Condition.php
+++ b/core/lib/Drupal/Core/Entity/Query/Sql/Condition.php
@@ -13,6 +13,13 @@ use Drupal\Core\Entity\Query\ConditionInterface;
class Condition extends ConditionBase {
/**
+ * Whether this condition is nested inside an OR condition.
+ *
+ * @var bool
+ */
+ protected $nestedInsideOrCondition = FALSE;
+
+ /**
* The SQL entity query object this condition belongs to.
*
* @var \Drupal\Core\Entity\Query\Sql\Query
@@ -36,11 +43,12 @@ class Condition extends ConditionBase {
$sql_condition = new SqlCondition($condition['field']->getConjunction());
// Add the SQL query to the object before calling this method again.
$sql_condition->sqlQuery = $sql_query;
+ $condition['field']->nestedInsideOrCondition = $this->nestedInsideOrCondition || strtoupper($this->conjunction) === 'OR';
$condition['field']->compile($sql_condition);
$conditionContainer->condition($sql_condition);
}
else {
- $type = strtoupper($this->conjunction) == 'OR' || $condition['operator'] == 'IS NULL' ? 'LEFT' : 'INNER';
+ $type = $this->nestedInsideOrCondition || strtoupper($this->conjunction) === 'OR' || $condition['operator'] === 'IS NULL' ? 'LEFT' : 'INNER';
$field = $tables->addField($condition['field'], $type, $condition['langcode']);
$condition['real_field'] = $field;
static::translateCondition($condition, $sql_query, $tables->isFieldCaseSensitive($condition['field']));
diff --git a/core/lib/Drupal/Core/Entity/Query/Sql/Query.php b/core/lib/Drupal/Core/Entity/Query/Sql/Query.php
index 50b81bb..7e5db5b 100644
--- a/core/lib/Drupal/Core/Entity/Query/Sql/Query.php
+++ b/core/lib/Drupal/Core/Entity/Query/Sql/Query.php
@@ -64,7 +64,6 @@ class Query extends QueryBase implements QueryInterface {
$this->connection = $connection;
}
-
/**
* {@inheritdoc}
*/
diff --git a/core/lib/Drupal/Core/Entity/Query/Sql/QueryAggregate.php b/core/lib/Drupal/Core/Entity/Query/Sql/QueryAggregate.php
index c791bb5..f865cf7 100644
--- a/core/lib/Drupal/Core/Entity/Query/Sql/QueryAggregate.php
+++ b/core/lib/Drupal/Core/Entity/Query/Sql/QueryAggregate.php
@@ -65,7 +65,6 @@ class QueryAggregate extends Query implements QueryAggregateInterface {
return $this->conditionAggregate->notExists($field, $function, $langcode);
}
-
/**
* Adds the aggregations to the query.
*
@@ -126,7 +125,6 @@ class QueryAggregate extends Query implements QueryAggregateInterface {
return $this;
}
-
/**
* Overrides \Drupal\Core\Entity\Query\Sql\Query::finish().
*
diff --git a/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php b/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php
index 0c54dc4..1f99d0f 100644
--- a/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php
+++ b/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php
@@ -94,9 +94,11 @@ class Tables implements TablesInterface {
$specifier = $specifiers[$key];
if (isset($field_storage_definitions[$specifier])) {
$field_storage = $field_storage_definitions[$specifier];
+ $column = $field_storage->getMainPropertyName();
}
else {
$field_storage = FALSE;
+ $column = NULL;
}
// If there is revision support, only the current revisions are being
@@ -121,8 +123,6 @@ class Tables implements TablesInterface {
// Check whether this field is stored in a dedicated table.
if ($field_storage && $table_mapping->requiresDedicatedTableStorage($field_storage)) {
$delta = NULL;
- // Find the field column.
- $column = $field_storage->getMainPropertyName();
if ($key < $count) {
$next = $specifiers[$key + 1];
@@ -176,10 +176,6 @@ class Tables implements TablesInterface {
}
$table = $this->ensureFieldTable($index_prefix, $field_storage, $type, $langcode, $base_table, $entity_id_field, $field_id_field, $delta);
$sql_column = $table_mapping->getFieldColumnName($field_storage, $column);
- $property_definitions = $field_storage->getPropertyDefinitions();
- if (isset($property_definitions[$column])) {
- $this->caseSensitiveFields[$field] = $property_definitions[$column]->getSetting('case_sensitive');
- }
}
// The field is stored in a shared table.
else {
@@ -188,6 +184,7 @@ class Tables implements TablesInterface {
// finds the property first. The data table is preferred, which is why
// it gets added before the base table.
$entity_tables = [];
+ $revision_table = NULL;
if ($all_revisions && $field_storage && $field_storage->isRevisionable()) {
$data_table = $entity_type->getRevisionDataTable();
$entity_base_table = $entity_type->getRevisionTable();
@@ -195,11 +192,18 @@ class Tables implements TablesInterface {
else {
$data_table = $entity_type->getDataTable();
$entity_base_table = $entity_type->getBaseTable();
+
+ if ($field_storage && $field_storage->isRevisionable() && in_array($field_storage->getName(), $entity_type->getRevisionMetadataKeys())) {
+ $revision_table = $entity_type->getRevisionTable();
+ }
}
if ($data_table) {
$this->sqlQuery->addMetaData('simple_query', FALSE);
$entity_tables[$data_table] = $this->getTableMapping($data_table, $entity_type_id);
}
+ if ($revision_table) {
+ $entity_tables[$revision_table] = $this->getTableMapping($revision_table, $entity_type_id);
+ }
$entity_tables[$entity_base_table] = $this->getTableMapping($entity_base_table, $entity_type_id);
$sql_column = $specifier;
@@ -239,18 +243,17 @@ class Tables implements TablesInterface {
}
$table = $this->ensureEntityTable($index_prefix, $sql_column, $type, $langcode, $base_table, $entity_id_field, $entity_tables);
+ }
- // If there is a field storage (some specifiers are not), check for case
- // sensitivity.
- if ($field_storage) {
- $column = $field_storage->getMainPropertyName();
- $base_field_property_definitions = $field_storage->getPropertyDefinitions();
- if (isset($base_field_property_definitions[$column])) {
- $this->caseSensitiveFields[$field] = $base_field_property_definitions[$column]->getSetting('case_sensitive');
- }
+ // If there is a field storage (some specifiers are not) and a field
+ // column, check for case sensitivity.
+ if ($field_storage && $column) {
+ $property_definitions = $field_storage->getPropertyDefinitions();
+ if (isset($property_definitions[$column])) {
+ $this->caseSensitiveFields[$field] = $property_definitions[$column]->getSetting('case_sensitive');
}
-
}
+
// If there are more specifiers to come, it's a relationship.
if ($field_storage && $key < $count) {
// Computed fields have prepared their property definition already, do
@@ -414,8 +417,10 @@ class Tables implements TablesInterface {
* @param string $table
* The table name.
*
- * @return array|bool
- * The table field mapping for the given table or FALSE if not available.
+ * @return array|false
+ * An associative array of table field mapping for the given table, keyed by
+ * columns name and values are just incrementing integers. If the table
+ * mapping is not available, FALSE is returned.
*/
protected function getTableMapping($table, $entity_type_id) {
$storage = $this->entityManager->getStorage($entity_type_id);
diff --git a/core/lib/Drupal/Core/Entity/RevisionableInterface.php b/core/lib/Drupal/Core/Entity/RevisionableInterface.php
index 14690e3..ee53e39 100644
--- a/core/lib/Drupal/Core/Entity/RevisionableInterface.php
+++ b/core/lib/Drupal/Core/Entity/RevisionableInterface.php
@@ -4,6 +4,21 @@ namespace Drupal\Core\Entity;
/**
* Provides methods for an entity to support revisions.
+ *
+ * Classes implementing this interface do not necessarily support revisions.
+ *
+ * To detect whether an entity type supports revisions, call
+ * EntityTypeInterface::isRevisionable().
+ *
+ * Many entity interfaces are composed of numerous other interfaces such as this
+ * one, which allow implementations to pick and choose which features to.
+ * support through stub implementations of various interface methods. This means
+ * that even if an entity class implements RevisionableInterface, it might only
+ * have a stub implementation and not a functional one.
+ *
+ * @see \Drupal\Core\Entity\EntityTypeInterface::isRevisionable()
+ * @see https://www.drupal.org/docs/8/api/entity-api/structure-of-an-entity-annotation
+ * @see https://www.drupal.org/docs/8/api/entity-api/making-an-entity-revisionable
*/
interface RevisionableInterface {
@@ -40,6 +55,27 @@ interface RevisionableInterface {
public function getRevisionId();
/**
+ * Gets the loaded Revision ID of the entity.
+ *
+ * @return int
+ * The loaded Revision identifier of the entity, or NULL if the entity
+ * does not have a revision identifier.
+ */
+ public function getLoadedRevisionId();
+
+ /**
+ * Updates the loaded Revision ID with the revision ID.
+ *
+ * This method should not be used, it could unintentionally cause the original
+ * revision ID property value to be lost.
+ *
+ * @internal
+ *
+ * @return $this
+ */
+ public function updateLoadedRevisionId();
+
+ /**
* Checks if this entity is the default revision.
*
* @param bool $new_value
@@ -52,6 +88,22 @@ interface RevisionableInterface {
public function isDefaultRevision($new_value = NULL);
/**
+ * Checks whether the entity object was a default revision when it was saved.
+ *
+ * @return bool
+ * TRUE if the entity object was a revision, FALSE otherwise.
+ */
+ public function wasDefaultRevision();
+
+ /**
+ * Checks if this entity is the latest revision.
+ *
+ * @return bool
+ * TRUE if the entity is the latest revision, FALSE otherwise.
+ */
+ public function isLatestRevision();
+
+ /**
* Acts on a revision before it gets saved.
*
* @param EntityStorageInterface $storage
diff --git a/core/lib/Drupal/Core/Entity/RevisionableStorageInterface.php b/core/lib/Drupal/Core/Entity/RevisionableStorageInterface.php
new file mode 100644
index 0000000..d23808b
--- /dev/null
+++ b/core/lib/Drupal/Core/Entity/RevisionableStorageInterface.php
@@ -0,0 +1,68 @@
+<?php
+
+namespace Drupal\Core\Entity;
+
+/**
+ * A storage that supports revisionable entity types.
+ */
+interface RevisionableStorageInterface {
+
+ /**
+ * Creates a new revision starting off from the specified entity object.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface|\Drupal\Core\Entity\RevisionableInterface $entity
+ * The revisionable entity object being modified.
+ * @param bool $default
+ * (optional) Whether the new revision should be marked as default. Defaults
+ * to TRUE.
+ *
+ * @return \Drupal\Core\Entity\EntityInterface|\Drupal\Core\Entity\RevisionableInterface
+ * A new entity revision object.
+ */
+ public function createRevision(RevisionableInterface $entity, $default = TRUE);
+
+ /**
+ * Loads a specific entity revision.
+ *
+ * @param int $revision_id
+ * The revision ID.
+ *
+ * @return \Drupal\Core\Entity\EntityInterface|null
+ * The specified entity revision or NULL if not found.
+ */
+ public function loadRevision($revision_id);
+
+ /**
+ * Loads multiple entity revisions.
+ *
+ * @param array $revision_ids
+ * An array of revision IDs to load.
+ *
+ * @return \Drupal\Core\Entity\EntityInterface[]
+ * An array of entity revisions keyed by their revision ID, or an empty
+ * array if none found.
+ */
+ public function loadMultipleRevisions(array $revision_ids);
+
+ /**
+ * Deletes a specific entity revision.
+ *
+ * A revision can only be deleted if it's not the currently active one.
+ *
+ * @param int $revision_id
+ * The revision ID.
+ */
+ public function deleteRevision($revision_id);
+
+ /**
+ * Returns the latest revision identifier for an entity.
+ *
+ * @param int|string $entity_id
+ * The entity identifier.
+ *
+ * @return int|string|null
+ * The latest revision identifier or NULL if no revision could be found.
+ */
+ public function getLatestRevisionId($entity_id);
+
+}
diff --git a/core/lib/Drupal/Core/Entity/Routing/AdminHtmlRouteProvider.php b/core/lib/Drupal/Core/Entity/Routing/AdminHtmlRouteProvider.php
index 1abee11..b04fc3e 100644
--- a/core/lib/Drupal/Core/Entity/Routing/AdminHtmlRouteProvider.php
+++ b/core/lib/Drupal/Core/Entity/Routing/AdminHtmlRouteProvider.php
@@ -54,4 +54,14 @@ class AdminHtmlRouteProvider extends DefaultHtmlRouteProvider {
}
}
+ /**
+ * {@inheritdoc}
+ */
+ protected function getDeleteMultipleFormRoute(EntityTypeInterface $entity_type) {
+ if ($route = parent::getDeleteMultipleFormRoute($entity_type)) {
+ $route->setOption('_admin_route', TRUE);
+ return $route;
+ }
+ }
+
}
diff --git a/core/lib/Drupal/Core/Entity/Routing/DefaultHtmlRouteProvider.php b/core/lib/Drupal/Core/Entity/Routing/DefaultHtmlRouteProvider.php
index fb40fd5..1415a1a 100644
--- a/core/lib/Drupal/Core/Entity/Routing/DefaultHtmlRouteProvider.php
+++ b/core/lib/Drupal/Core/Entity/Routing/DefaultHtmlRouteProvider.php
@@ -24,6 +24,7 @@ use Symfony\Component\Routing\RouteCollection;
* - edit-form
* - delete-form
* - collection
+ * - delete-multiple-form
*
* @see \Drupal\Core\Entity\Routing\AdminHtmlRouteProvider.
*/
@@ -98,6 +99,10 @@ class DefaultHtmlRouteProvider implements EntityRouteProviderInterface, EntityHa
$collection->add("entity.{$entity_type_id}.collection", $collection_route);
}
+ if ($delete_multiple_route = $this->getDeleteMultipleFormRoute($entity_type)) {
+ $collection->add('entity.' . $entity_type->id() . '.delete_multiple_form', $delete_multiple_route);
+ }
+
return $collection;
}
@@ -253,7 +258,7 @@ class DefaultHtmlRouteProvider implements EntityRouteProviderInterface, EntityHa
$route
->setDefaults([
'_entity_form' => "{$entity_type_id}.{$operation}",
- '_title_callback' => '\Drupal\Core\Entity\Controller\EntityController::editTitle'
+ '_title_callback' => '\Drupal\Core\Entity\Controller\EntityController::editTitle',
])
->setRequirement('_entity_access', "{$entity_type_id}.update")
->setOption('parameters', [
@@ -350,4 +355,23 @@ class DefaultHtmlRouteProvider implements EntityRouteProviderInterface, EntityHa
return $field_storage_definitions[$entity_type->getKey('id')]->getType();
}
+ /**
+ * Returns the delete multiple form route.
+ *
+ * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+ * The entity type.
+ *
+ * @return \Symfony\Component\Routing\Route|null
+ * The generated route, if available.
+ */
+ protected function getDeleteMultipleFormRoute(EntityTypeInterface $entity_type) {
+ if ($entity_type->hasLinkTemplate('delete-multiple-form') && $entity_type->hasHandlerClass('form', 'delete-multiple-confirm')) {
+ $route = new Route($entity_type->getLinkTemplate('delete-multiple-form'));
+ $route->setDefault('_form', $entity_type->getFormClass('delete-multiple-confirm'));
+ $route->setDefault('entity_type_id', $entity_type->id());
+ $route->setRequirement('_entity_delete_multiple_access', $entity_type->id());
+ return $route;
+ }
+ }
+
}
diff --git a/core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php b/core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php
index fdc7289..d6ca5db 100644
--- a/core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php
+++ b/core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php
@@ -25,6 +25,35 @@ class DefaultTableMapping implements TableMappingInterface {
protected $fieldStorageDefinitions = [];
/**
+ * The base table of the entity.
+ *
+ * @var string
+ */
+ protected $baseTable;
+
+ /**
+ * The table that stores revisions, if the entity supports revisions.
+ *
+ * @var string
+ */
+ protected $revisionTable;
+
+ /**
+ * The table that stores field data, if the entity has multilingual support.
+ *
+ * @var string
+ */
+ protected $dataTable;
+
+ /**
+ * The table that stores revision field data if the entity supports revisions
+ * and has multilingual support.
+ *
+ * @var string
+ */
+ protected $revisionDataTable;
+
+ /**
* A list of field names per table.
*
* This corresponds to the return value of
@@ -87,6 +116,180 @@ class DefaultTableMapping implements TableMappingInterface {
public function __construct(ContentEntityTypeInterface $entity_type, array $storage_definitions) {
$this->entityType = $entity_type;
$this->fieldStorageDefinitions = $storage_definitions;
+
+ // @todo Remove table names from the entity type definition in
+ // https://www.drupal.org/node/2232465.
+ $this->baseTable = $entity_type->getBaseTable() ?: $entity_type->id();
+ if ($entity_type->isRevisionable()) {
+ $this->revisionTable = $entity_type->getRevisionTable() ?: $entity_type->id() . '_revision';
+ }
+ if ($entity_type->isTranslatable()) {
+ $this->dataTable = $entity_type->getDataTable() ?: $entity_type->id() . '_field_data';
+ }
+ if ($entity_type->isRevisionable() && $entity_type->isTranslatable()) {
+ $this->revisionDataTable = $entity_type->getRevisionDataTable() ?: $entity_type->id() . '_field_revision';
+ }
+ }
+
+ /**
+ * Initializes the table mapping.
+ *
+ * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
+ * The entity type definition.
+ * @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $storage_definitions
+ * A list of field storage definitions that should be available for the
+ * field columns of this table mapping.
+ *
+ * @return static
+ *
+ * @internal
+ */
+ public static function create(ContentEntityTypeInterface $entity_type, array $storage_definitions) {
+ $table_mapping = new static($entity_type, $storage_definitions);
+
+ $revisionable = $entity_type->isRevisionable();
+ $translatable = $entity_type->isTranslatable();
+
+ $id_key = $entity_type->getKey('id');
+ $revision_key = $entity_type->getKey('revision');
+ $bundle_key = $entity_type->getKey('bundle');
+ $uuid_key = $entity_type->getKey('uuid');
+ $langcode_key = $entity_type->getKey('langcode');
+
+ $shared_table_definitions = array_filter($storage_definitions, function (FieldStorageDefinitionInterface $definition) use ($table_mapping) {
+ return $table_mapping->allowsSharedTableStorage($definition);
+ });
+
+ $key_fields = array_values(array_filter([$id_key, $revision_key, $bundle_key, $uuid_key, $langcode_key]));
+ $all_fields = array_keys($shared_table_definitions);
+ $revisionable_fields = array_keys(array_filter($shared_table_definitions, function (FieldStorageDefinitionInterface $definition) {
+ return $definition->isRevisionable();
+ }));
+ // Make sure the key fields come first in the list of fields.
+ $all_fields = array_merge($key_fields, array_diff($all_fields, $key_fields));
+
+ $revision_metadata_fields = $revisionable ? array_values($entity_type->getRevisionMetadataKeys()) : [];
+
+ if (!$revisionable && !$translatable) {
+ // The base layout stores all the base field values in the base table.
+ $table_mapping->setFieldNames($table_mapping->baseTable, $all_fields);
+ }
+ elseif ($revisionable && !$translatable) {
+ // The revisionable layout stores all the base field values in the base
+ // table, except for revision metadata fields. Revisionable fields
+ // denormalized in the base table but also stored in the revision table
+ // together with the entity ID and the revision ID as identifiers.
+ $table_mapping->setFieldNames($table_mapping->baseTable, array_diff($all_fields, $revision_metadata_fields));
+ $revision_key_fields = [$id_key, $revision_key];
+ $table_mapping->setFieldNames($table_mapping->revisionTable, array_merge($revision_key_fields, $revisionable_fields));
+ }
+ elseif (!$revisionable && $translatable) {
+ // Multilingual layouts store key field values in the base table. The
+ // other base field values are stored in the data table, no matter
+ // whether they are translatable or not. The data table holds also a
+ // denormalized copy of the bundle field value to allow for more
+ // performant queries. This means that only the UUID is not stored on
+ // the data table.
+ $table_mapping
+ ->setFieldNames($table_mapping->baseTable, $key_fields)
+ ->setFieldNames($table_mapping->dataTable, array_values(array_diff($all_fields, [$uuid_key])));
+ }
+ elseif ($revisionable && $translatable) {
+ // The revisionable multilingual layout stores key field values in the
+ // base table and the revision table holds the entity ID, revision ID and
+ // langcode ID along with revision metadata. The revision data table holds
+ // data field values for all the revisionable fields and the data table
+ // holds the data field values for all non-revisionable fields. The data
+ // field values of revisionable fields are denormalized in the data
+ // table, as well.
+ $table_mapping->setFieldNames($table_mapping->baseTable, $key_fields);
+
+ // Like in the multilingual, non-revisionable case the UUID is not
+ // in the data table. Additionally, do not store revision metadata
+ // fields in the data table.
+ $data_fields = array_values(array_diff($all_fields, [$uuid_key], $revision_metadata_fields));
+ $table_mapping->setFieldNames($table_mapping->dataTable, $data_fields);
+
+ $revision_base_fields = array_merge([$id_key, $revision_key, $langcode_key], $revision_metadata_fields);
+ $table_mapping->setFieldNames($table_mapping->revisionTable, $revision_base_fields);
+
+ $revision_data_key_fields = [$id_key, $revision_key, $langcode_key];
+ $revision_data_fields = array_diff($revisionable_fields, $revision_metadata_fields, [$langcode_key]);
+ $table_mapping->setFieldNames($table_mapping->revisionDataTable, array_merge($revision_data_key_fields, $revision_data_fields));
+ }
+
+ // Add dedicated tables.
+ $dedicated_table_definitions = array_filter($table_mapping->fieldStorageDefinitions, function (FieldStorageDefinitionInterface $definition) use ($table_mapping) {
+ return $table_mapping->requiresDedicatedTableStorage($definition);
+ });
+ $extra_columns = [
+ 'bundle',
+ 'deleted',
+ 'entity_id',
+ 'revision_id',
+ 'langcode',
+ 'delta',
+ ];
+ foreach ($dedicated_table_definitions as $field_name => $definition) {
+ $tables = [$table_mapping->getDedicatedDataTableName($definition)];
+ if ($revisionable && $definition->isRevisionable()) {
+ $tables[] = $table_mapping->getDedicatedRevisionTableName($definition);
+ }
+ foreach ($tables as $table_name) {
+ $table_mapping->setFieldNames($table_name, [$field_name]);
+ $table_mapping->setExtraColumns($table_name, $extra_columns);
+ }
+ }
+
+ return $table_mapping;
+ }
+
+ /**
+ * Gets the base table name.
+ *
+ * @return string
+ * The base table name.
+ *
+ * @internal
+ */
+ public function getBaseTable() {
+ return $this->baseTable;
+ }
+
+ /**
+ * Gets the revision table name.
+ *
+ * @return string|null
+ * The revision table name.
+ *
+ * @internal
+ */
+ public function getRevisionTable() {
+ return $this->revisionTable;
+ }
+
+ /**
+ * Gets the data table name.
+ *
+ * @return string|null
+ * The data table name.
+ *
+ * @internal
+ */
+ public function getDataTable() {
+ return $this->dataTable;
+ }
+
+ /**
+ * Gets the revision data table name.
+ *
+ * @return string|null
+ * The revision data table name.
+ *
+ * @internal
+ */
+ public function getRevisionDataTable() {
+ return $this->revisionDataTable;
}
/**
@@ -143,17 +346,13 @@ class DefaultTableMapping implements TableMappingInterface {
// where field data is stored, otherwise the base table is responsible for
// storing field data. Revision metadata is an exception as it's stored
// only in the revision table.
- // @todo The table mapping itself should know about entity tables. See
- // https://www.drupal.org/node/2274017.
- /** @var \Drupal\Core\Entity\Sql\SqlContentEntityStorage $storage */
- $storage = \Drupal::entityManager()->getStorage($this->entityType->id());
$storage_definition = $this->fieldStorageDefinitions[$field_name];
- $table_names = [
- $storage->getDataTable(),
- $storage->getBaseTable(),
- $storage->getRevisionTable(),
+ $table_names = array_filter([
+ $this->dataTable,
+ $this->baseTable,
+ $this->revisionTable,
$this->getDedicatedDataTableName($storage_definition),
- ];
+ ]);
// Collect field columns.
$field_columns = [];
@@ -161,7 +360,7 @@ class DefaultTableMapping implements TableMappingInterface {
$field_columns[] = $this->getFieldColumnName($storage_definition, $property_name);
}
- foreach (array_filter($table_names) as $table_name) {
+ foreach ($table_names as $table_name) {
$columns = $this->getAllColumns($table_name);
// We assume finding one field column belonging to the mapping is enough
// to identify the field table.
@@ -227,6 +426,10 @@ class DefaultTableMapping implements TableMappingInterface {
* A list of field names to add the columns for.
*
* @return $this
+ *
+ * @deprecated in Drupal 8.6.0 and will be changed to a protected method
+ * before Drupal 9.0.0. There will be no replacement for it because the
+ * default table mapping is now able to be initialized on its own.
*/
public function setFieldNames($table_name, array $field_names) {
$this->fieldNames[$table_name] = $field_names;
@@ -254,6 +457,10 @@ class DefaultTableMapping implements TableMappingInterface {
* The list of column names.
*
* @return $this
+ *
+ * @deprecated in Drupal 8.6.0 and will be changed to a protected method
+ * before Drupal 9.0.0. There will be no replacement for it because the
+ * default table mapping is now able to be initialized on its own.
*/
public function setExtraColumns($table_name, array $column_names) {
$this->extraColumns[$table_name] = $column_names;
@@ -269,10 +476,10 @@ class DefaultTableMapping implements TableMappingInterface {
* The field storage definition.
*
* @return bool
- * TRUE if the field can be stored in a dedicated table, FALSE otherwise.
+ * TRUE if the field can be stored in a shared table, FALSE otherwise.
*/
public function allowsSharedTableStorage(FieldStorageDefinitionInterface $storage_definition) {
- return !$storage_definition->hasCustomStorage() && $storage_definition->isBaseField() && !$storage_definition->isMultiple();
+ return !$storage_definition->hasCustomStorage() && $storage_definition->isBaseField() && !$storage_definition->isMultiple() && !$storage_definition->isDeleted();
}
/**
@@ -282,7 +489,7 @@ class DefaultTableMapping implements TableMappingInterface {
* The field storage definition.
*
* @return bool
- * TRUE if the field can be stored in a dedicated table, FALSE otherwise.
+ * TRUE if the field has to be stored in a dedicated table, FALSE otherwise.
*/
public function requiresDedicatedTableStorage(FieldStorageDefinitionInterface $storage_definition) {
return !$storage_definition->hasCustomStorage() && !$this->allowsSharedTableStorage($storage_definition);
@@ -331,8 +538,8 @@ class DefaultTableMapping implements TableMappingInterface {
public function getDedicatedDataTableName(FieldStorageDefinitionInterface $storage_definition, $is_deleted = FALSE) {
if ($is_deleted) {
// When a field is a deleted, the table is renamed to
- // {field_deleted_data_FIELD_UUID}. To make sure we don't end up with
- // table names longer than 64 characters, we hash the unique storage
+ // {field_deleted_data_UNIQUE_STORAGE_ID}. To make sure we don't end up
+ // with table names longer than 64 characters, we hash the unique storage
// identifier and return the first 10 characters so we end up with a short
// unique ID.
return "field_deleted_data_" . substr(hash('sha256', $storage_definition->getUniqueStorageIdentifier()), 0, 10);
@@ -357,10 +564,10 @@ class DefaultTableMapping implements TableMappingInterface {
public function getDedicatedRevisionTableName(FieldStorageDefinitionInterface $storage_definition, $is_deleted = FALSE) {
if ($is_deleted) {
// When a field is a deleted, the table is renamed to
- // {field_deleted_revision_FIELD_UUID}. To make sure we don't end up with
- // table names longer than 64 characters, we hash the unique storage
- // identifier and return the first 10 characters so we end up with a short
- // unique ID.
+ // {field_deleted_revision_UNIQUE_STORAGE_ID}. To make sure we don't end
+ // up with table names longer than 64 characters, we hash the unique
+ // storage identifier and return the first 10 characters so we end up with
+ // a short unique ID.
return "field_deleted_revision_" . substr(hash('sha256', $storage_definition->getUniqueStorageIdentifier()), 0, 10);
}
else {
@@ -389,7 +596,7 @@ class DefaultTableMapping implements TableMappingInterface {
// prefixes.
if (strlen($table_name) > 48) {
// Use a shorter separator, a truncated entity_type, and a hash of the
- // field UUID.
+ // field storage unique identifier.
$separator = $revision ? '_r__' : '__';
// Truncate to the same length for the current and revision tables.
$entity_type = substr($storage_definition->getTargetEntityTypeId(), 0, 34);
diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
index 22238d4..48545d2 100644
--- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
+++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
@@ -3,12 +3,14 @@
namespace Drupal\Core\Entity\Sql;
use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Cache\MemoryCache\MemoryCacheInterface;
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\Database;
use Drupal\Core\Database\DatabaseExceptionWrapper;
use Drupal\Core\Database\SchemaException;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\ContentEntityStorageBase;
+use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\Core\Entity\EntityBundleListenerInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityManagerInterface;
@@ -19,7 +21,6 @@ use Drupal\Core\Entity\Schema\DynamicallyFieldableEntityStorageSchemaInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Language\LanguageInterface;
-use Drupal\field\FieldStorageConfigInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -133,7 +134,8 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
$container->get('database'),
$container->get('entity.manager'),
$container->get('cache.entity'),
- $container->get('language_manager')
+ $container->get('language_manager'),
+ $container->get('entity.memory_cache')
);
}
@@ -161,9 +163,11 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
* The cache backend to be used.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
+ * @param \Drupal\Core\Cache\MemoryCache\MemoryCacheInterface|null $memory_cache
+ * The memory cache backend to be used.
*/
- public function __construct(EntityTypeInterface $entity_type, Connection $database, EntityManagerInterface $entity_manager, CacheBackendInterface $cache, LanguageManagerInterface $language_manager) {
- parent::__construct($entity_type, $entity_manager, $cache);
+ public function __construct(EntityTypeInterface $entity_type, Connection $database, EntityManagerInterface $entity_manager, CacheBackendInterface $cache, LanguageManagerInterface $language_manager, MemoryCacheInterface $memory_cache = NULL) {
+ parent::__construct($entity_type, $entity_manager, $cache, $memory_cache);
$this->database = $database;
$this->languageManager = $language_manager;
$this->initTableLayout();
@@ -181,22 +185,21 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
$this->dataTable = NULL;
$this->revisionDataTable = NULL;
- // @todo Remove table names from the entity type definition in
- // https://www.drupal.org/node/2232465.
- $this->baseTable = $this->entityType->getBaseTable() ?: $this->entityTypeId;
+ $table_mapping = $this->getTableMapping();
+ $this->baseTable = $table_mapping->getBaseTable();
$revisionable = $this->entityType->isRevisionable();
if ($revisionable) {
$this->revisionKey = $this->entityType->getKey('revision') ?: 'revision_id';
- $this->revisionTable = $this->entityType->getRevisionTable() ?: $this->entityTypeId . '_revision';
+ $this->revisionTable = $table_mapping->getRevisionTable();
}
$translatable = $this->entityType->isTranslatable();
if ($translatable) {
- $this->dataTable = $this->entityType->getDataTable() ?: $this->entityTypeId . '_field_data';
+ $this->dataTable = $table_mapping->getDataTable();
$this->langcodeKey = $this->entityType->getKey('langcode');
$this->defaultLangcodeKey = $this->entityType->getKey('default_langcode');
}
if ($revisionable && $translatable) {
- $this->revisionDataTable = $this->entityType->getRevisionDataTable() ?: $this->entityTypeId . '_field_revision';
+ $this->revisionDataTable = $table_mapping->getRevisionDataTable();
}
}
@@ -284,6 +287,11 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
*/
public function setTableMapping(TableMappingInterface $table_mapping) {
$this->tableMapping = $table_mapping;
+
+ $this->baseTable = $table_mapping->getBaseTable();
+ $this->revisionTable = $table_mapping->getRevisionTable();
+ $this->dataTable = $table_mapping->getDataTable();
+ $this->revisionDataTable = $table_mapping->getRevisionDataTable();
}
/**
@@ -302,117 +310,41 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
* {@inheritdoc}
*/
public function getTableMapping(array $storage_definitions = NULL) {
- $table_mapping = $this->tableMapping;
+ // If a new set of field storage definitions is passed, for instance when
+ // comparing old and new storage schema, we compute the table mapping
+ // without caching.
+ if ($storage_definitions) {
+ return $this->getCustomTableMapping($this->entityType, $storage_definitions);
+ }
// If we are using our internal storage definitions, which is our main use
- // case, we can statically cache the computed table mapping. If a new set
- // of field storage definitions is passed, for instance when comparing old
- // and new storage schema, we compute the table mapping without caching.
- // @todo Clean-up this in https://www.drupal.org/node/2274017 so we can
- // easily instantiate a new table mapping whenever needed.
- if (!isset($this->tableMapping) || $storage_definitions) {
- $table_mapping_class = $this->temporary ? TemporaryTableMapping::class : DefaultTableMapping::class;
- $definitions = $storage_definitions ?: $this->entityManager->getFieldStorageDefinitions($this->entityTypeId);
- /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping|\Drupal\Core\Entity\Sql\TemporaryTableMapping $table_mapping */
- $table_mapping = new $table_mapping_class($this->entityType, $definitions);
-
- $shared_table_definitions = array_filter($definitions, function (FieldStorageDefinitionInterface $definition) use ($table_mapping) {
- return $table_mapping->allowsSharedTableStorage($definition);
- });
-
- $key_fields = array_values(array_filter([$this->idKey, $this->revisionKey, $this->bundleKey, $this->uuidKey, $this->langcodeKey]));
- $all_fields = array_keys($shared_table_definitions);
- $revisionable_fields = array_keys(array_filter($shared_table_definitions, function (FieldStorageDefinitionInterface $definition) {
- return $definition->isRevisionable();
- }));
- // Make sure the key fields come first in the list of fields.
- $all_fields = array_merge($key_fields, array_diff($all_fields, $key_fields));
-
- // If the entity is revisionable, gather the fields that need to be put
- // in the revision table.
- $revisionable = $this->entityType->isRevisionable();
- $revision_metadata_fields = $revisionable ? array_values($this->entityType->getRevisionMetadataKeys()) : [];
-
- $translatable = $this->entityType->isTranslatable();
- if (!$revisionable && !$translatable) {
- // The base layout stores all the base field values in the base table.
- $table_mapping->setFieldNames($this->baseTable, $all_fields);
- }
- elseif ($revisionable && !$translatable) {
- // The revisionable layout stores all the base field values in the base
- // table, except for revision metadata fields. Revisionable fields
- // denormalized in the base table but also stored in the revision table
- // together with the entity ID and the revision ID as identifiers.
- $table_mapping->setFieldNames($this->baseTable, array_diff($all_fields, $revision_metadata_fields));
- $revision_key_fields = [$this->idKey, $this->revisionKey];
- $table_mapping->setFieldNames($this->revisionTable, array_merge($revision_key_fields, $revisionable_fields));
- }
- elseif (!$revisionable && $translatable) {
- // Multilingual layouts store key field values in the base table. The
- // other base field values are stored in the data table, no matter
- // whether they are translatable or not. The data table holds also a
- // denormalized copy of the bundle field value to allow for more
- // performant queries. This means that only the UUID is not stored on
- // the data table.
- $table_mapping
- ->setFieldNames($this->baseTable, $key_fields)
- ->setFieldNames($this->dataTable, array_values(array_diff($all_fields, [$this->uuidKey])));
- }
- elseif ($revisionable && $translatable) {
- // The revisionable multilingual layout stores key field values in the
- // base table, except for language, which is stored in the revision
- // table along with revision metadata. The revision data table holds
- // data field values for all the revisionable fields and the data table
- // holds the data field values for all non-revisionable fields. The data
- // field values of revisionable fields are denormalized in the data
- // table, as well.
- $table_mapping->setFieldNames($this->baseTable, array_values($key_fields));
-
- // Like in the multilingual, non-revisionable case the UUID is not
- // in the data table. Additionally, do not store revision metadata
- // fields in the data table.
- $data_fields = array_values(array_diff($all_fields, [$this->uuidKey], $revision_metadata_fields));
- $table_mapping->setFieldNames($this->dataTable, $data_fields);
-
- $revision_base_fields = array_merge([$this->idKey, $this->revisionKey, $this->langcodeKey], $revision_metadata_fields);
- $table_mapping->setFieldNames($this->revisionTable, $revision_base_fields);
-
- $revision_data_key_fields = [$this->idKey, $this->revisionKey, $this->langcodeKey];
- $revision_data_fields = array_diff($revisionable_fields, $revision_metadata_fields, [$this->langcodeKey]);
- $table_mapping->setFieldNames($this->revisionDataTable, array_merge($revision_data_key_fields, $revision_data_fields));
- }
-
- // Add dedicated tables.
- $dedicated_table_definitions = array_filter($definitions, function (FieldStorageDefinitionInterface $definition) use ($table_mapping) {
- return $table_mapping->requiresDedicatedTableStorage($definition);
- });
- $extra_columns = [
- 'bundle',
- 'deleted',
- 'entity_id',
- 'revision_id',
- 'langcode',
- 'delta',
- ];
- foreach ($dedicated_table_definitions as $field_name => $definition) {
- $tables = [$table_mapping->getDedicatedDataTableName($definition)];
- if ($revisionable && $definition->isRevisionable()) {
- $tables[] = $table_mapping->getDedicatedRevisionTableName($definition);
- }
- foreach ($tables as $table_name) {
- $table_mapping->setFieldNames($table_name, [$field_name]);
- $table_mapping->setExtraColumns($table_name, $extra_columns);
- }
- }
+ // case, we can statically cache the computed table mapping.
+ if (!isset($this->tableMapping)) {
+ $storage_definitions = $this->entityManager->getFieldStorageDefinitions($this->entityTypeId);
- // Cache the computed table mapping only if we are using our internal
- // storage definitions.
- if (!$storage_definitions) {
- $this->tableMapping = $table_mapping;
- }
+ $this->tableMapping = $this->getCustomTableMapping($this->entityType, $storage_definitions);
}
- return $table_mapping;
+ return $this->tableMapping;
+ }
+
+ /**
+ * Gets a table mapping for the specified entity type and storage definitions.
+ *
+ * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
+ * An entity type definition.
+ * @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $storage_definitions
+ * An array of field storage definitions to be used to compute the table
+ * mapping.
+ *
+ * @return \Drupal\Core\Entity\Sql\TableMappingInterface
+ * A table mapping object for the entity's tables.
+ *
+ * @internal
+ */
+ public function getCustomTableMapping(ContentEntityTypeInterface $entity_type, array $storage_definitions) {
+ $table_mapping_class = $this->temporary ? TemporaryTableMapping::class : DefaultTableMapping::class;
+ return $table_mapping_class::create($entity_type, $storage_definitions);
}
/**
@@ -469,9 +401,11 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
* Maps from storage records to entity objects, and attaches fields.
*
* @param array $records
- * Associative array of query results, keyed on the entity ID.
+ * Associative array of query results, keyed on the entity ID or revision
+ * ID.
* @param bool $load_from_revision
- * Flag to indicate whether revisions should be loaded or not.
+ * (optional) Flag to indicate whether revisions should be loaded or not.
+ * Defaults to FALSE.
*
* @return array
* An array of entity objects implementing the EntityInterface.
@@ -481,32 +415,53 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
return [];
}
+ // Get the names of the fields that are stored in the base table and, if
+ // applicable, the revision table. Other entity data will be loaded in
+ // loadFromSharedTables() and loadFromDedicatedTables().
+ $field_names = $this->tableMapping->getFieldNames($this->baseTable);
+ if ($this->revisionTable) {
+ $field_names = array_unique(array_merge($field_names, $this->tableMapping->getFieldNames($this->revisionTable)));
+ }
+
$values = [];
foreach ($records as $id => $record) {
$values[$id] = [];
// Skip the item delta and item value levels (if possible) but let the
// field assign the value as suiting. This avoids unnecessary array
// hierarchies and saves memory here.
- foreach ($record as $name => $value) {
- // Handle columns named [field_name]__[column_name] (e.g for field types
- // that store several properties).
- if ($field_name = strstr($name, '__', TRUE)) {
- $property_name = substr($name, strpos($name, '__') + 2);
- $values[$id][$field_name][LanguageInterface::LANGCODE_DEFAULT][$property_name] = $value;
+ foreach ($field_names as $field_name) {
+ $field_columns = $this->tableMapping->getColumnNames($field_name);
+ // Handle field types that store several properties.
+ if (count($field_columns) > 1) {
+ foreach ($field_columns as $property_name => $column_name) {
+ if (property_exists($record, $column_name)) {
+ $values[$id][$field_name][LanguageInterface::LANGCODE_DEFAULT][$property_name] = $record->{$column_name};
+ unset($record->{$column_name});
+ }
+ }
}
+ // Handle field types that store only one property.
else {
- // Handle columns named directly after the field (e.g if the field
- // type only stores one property).
- $values[$id][$name][LanguageInterface::LANGCODE_DEFAULT] = $value;
+ $column_name = reset($field_columns);
+ if (property_exists($record, $column_name)) {
+ $values[$id][$field_name][LanguageInterface::LANGCODE_DEFAULT] = $record->{$column_name};
+ unset($record->{$column_name});
+ }
}
}
+
+ // Handle additional record entries that are not provided by an entity
+ // field, such as 'isDefaultRevision'.
+ foreach ($record as $name => $value) {
+ $values[$id][$name][LanguageInterface::LANGCODE_DEFAULT] = $value;
+ }
}
// Initialize translations array.
$translations = array_fill_keys(array_keys($values), []);
// Load values from shared and dedicated tables.
- $this->loadFromSharedTables($values, $translations);
+ $this->loadFromSharedTables($values, $translations, $load_from_revision);
$this->loadFromDedicatedTables($values, $load_from_revision);
$entities = [];
@@ -523,11 +478,15 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
* Loads values for fields stored in the shared data tables.
*
* @param array &$values
- * Associative array of entities values, keyed on the entity ID.
+ * Associative array of entities values, keyed on the entity ID or the
+ * revision ID.
* @param array &$translations
* List of translations, keyed on the entity ID.
+ * @param bool $load_from_revision
+ * Flag to indicate whether revisions should be loaded or not.
*/
- protected function loadFromSharedTables(array &$values, array &$translations) {
+ protected function loadFromSharedTables(array &$values, array &$translations, $load_from_revision) {
+ $record_key = !$load_from_revision ? $this->idKey : $this->revisionKey;
if ($this->dataTable) {
// If a revision table is available, we need all the properties of the
// latest revision. Otherwise we fall back to the data table.
@@ -535,8 +494,8 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
$alias = $this->revisionDataTable ? 'revision' : 'data';
$query = $this->database->select($table, $alias, ['fetch' => \PDO::FETCH_ASSOC])
->fields($alias)
- ->condition($alias . '.' . $this->idKey, array_keys($values), 'IN')
- ->orderBy($alias . '.' . $this->idKey);
+ ->condition($alias . '.' . $record_key, array_keys($values), 'IN')
+ ->orderBy($alias . '.' . $record_key);
$table_mapping = $this->getTableMapping();
if ($this->revisionDataTable) {
@@ -559,7 +518,7 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
// Some fields can have more then one columns in the data table so
// column names are needed.
foreach ($data_fields as $data_field) {
- // \Drupal\Core\Entity\Sql\TableMappingInterface:: getColumNames()
+ // \Drupal\Core\Entity\Sql\TableMappingInterface::getColumnNames()
// returns an array keyed by property names so remove the keys
// before array_merge() to avoid losing data with fields having the
// same columns i.e. value.
@@ -581,7 +540,7 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
$result = $query->execute();
foreach ($result as $row) {
- $id = $row[$this->idKey];
+ $id = $row[$record_key];
// Field values in default language are stored with
// LanguageInterface::LANGCODE_DEFAULT as key.
@@ -609,19 +568,35 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
* {@inheritdoc}
*/
protected function doLoadRevisionFieldItems($revision_id) {
- $revision = NULL;
+ @trigger_error('"\Drupal\Core\Entity\ContentEntityStorageBase::doLoadRevisionFieldItems()" is deprecated in Drupal 8.5.x and will be removed before Drupal 9.0.0. "\Drupal\Core\Entity\ContentEntityStorageBase::doLoadMultipleRevisionsFieldItems()" should be implemented instead. See https://www.drupal.org/node/2924915.', E_USER_DEPRECATED);
+
+ $revisions = $this->doLoadMultipleRevisionsFieldItems([$revision_id]);
+
+ return !empty($revisions) ? reset($revisions) : NULL;
+ }
- // Build and execute the query.
- $query_result = $this->buildQuery([], $revision_id)->execute();
- $records = $query_result->fetchAllAssoc($this->idKey);
+ /**
+ * {@inheritdoc}
+ */
+ protected function doLoadMultipleRevisionsFieldItems($revision_ids) {
+ $revisions = [];
+
+ // Sanitize IDs. Before feeding ID array into buildQuery, check whether
+ // it is empty as this would load all entity revisions.
+ $revision_ids = $this->cleanIds($revision_ids, 'revision');
- if (!empty($records)) {
- // Convert the raw records to entity objects.
- $entities = $this->mapFromStorageRecords($records, TRUE);
- $revision = reset($entities) ?: NULL;
+ if (!empty($revision_ids)) {
+ // Build and execute the query.
+ $query_result = $this->buildQuery(NULL, $revision_ids)->execute();
+ $records = $query_result->fetchAllAssoc($this->revisionKey);
+
+ // Map the loaded records into entity objects and according fields.
+ if ($records) {
+ $revisions = $this->mapFromStorageRecords($records, TRUE);
+ }
}
- return $revision;
+ return $revisions;
}
/**
@@ -677,20 +652,23 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
*
* @param array|null $ids
* An array of entity IDs, or NULL to load all entities.
- * @param $revision_id
- * The ID of the revision to load, or FALSE if this query is asking for the
- * most current revision(s).
+ * @param array|bool $revision_ids
+ * The IDs of the revisions to load, or FALSE if this query is asking for
+ * the default revisions. Defaults to FALSE.
*
* @return \Drupal\Core\Database\Query\Select
* A SelectQuery object for loading the entity.
*/
- protected function buildQuery($ids, $revision_id = FALSE) {
- $query = $this->database->select($this->entityType->getBaseTable(), 'base');
+ protected function buildQuery($ids, $revision_ids = FALSE) {
+ $query = $this->database->select($this->baseTable, 'base');
$query->addTag($this->entityTypeId . '_load_multiple');
- if ($revision_id) {
- $query->join($this->revisionTable, 'revision', "revision.{$this->idKey} = base.{$this->idKey} AND revision.{$this->revisionKey} = :revisionId", [':revisionId' => $revision_id]);
+ if ($revision_ids) {
+ if (!is_array($revision_ids)) {
+ @trigger_error('Passing a single revision ID to "\Drupal\Core\Entity\Sql\SqlContentEntityStorage::buildQuery()" is deprecated in Drupal 8.5.x and will be removed before Drupal 9.0.0. An array of revision IDs should be given instead. See https://www.drupal.org/node/2924915.', E_USER_DEPRECATED);
+ }
+ $query->join($this->revisionTable, 'revision', "revision.{$this->idKey} = base.{$this->idKey} AND revision.{$this->revisionKey} IN (:revisionIds[])", [':revisionIds[]' => (array) $revision_ids]);
}
elseif ($this->revisionTable) {
$query->join($this->revisionTable, 'revision', "revision.{$this->revisionKey} = base.{$this->revisionKey}");
@@ -760,7 +738,7 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
protected function doDeleteFieldItems($entities) {
$ids = array_keys($entities);
- $this->database->delete($this->entityType->getBaseTable())
+ $this->database->delete($this->baseTable)
->condition($this->idKey, $ids, 'IN')
->execute();
@@ -843,10 +821,13 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
if ($update) {
$default_revision = $entity->isDefaultRevision();
if ($default_revision) {
+ // Remove the ID from the record to enable updates on SQL variants
+ // that prevent updating serial columns, for example, mssql.
+ unset($record->{$this->idKey});
$this->database
->update($this->baseTable)
->fields((array) $record)
- ->condition($this->idKey, $record->{$this->idKey})
+ ->condition($this->idKey, $entity->get($this->idKey)->value)
->execute();
}
if ($this->revisionTable) {
@@ -855,11 +836,15 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
}
else {
$record = $this->mapToStorageRecord($entity->getUntranslated(), $this->revisionTable);
+ // Remove the revision ID from the record to enable updates on SQL
+ // variants that prevent updating serial columns, for example,
+ // mssql.
+ unset($record->{$this->revisionKey});
$entity->preSaveRevision($this, $record);
$this->database
->update($this->revisionTable)
->fields((array) $record)
- ->condition($this->revisionKey, $record->{$this->revisionKey})
+ ->condition($this->revisionKey, $entity->getRevisionId())
->execute();
}
}
@@ -1081,24 +1066,26 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
$record->{$this->revisionKey} = $insert_id;
}
if ($entity->isDefaultRevision()) {
- $this->database->update($this->entityType->getBaseTable())
+ $this->database->update($this->baseTable)
->fields([$this->revisionKey => $record->{$this->revisionKey}])
->condition($this->idKey, $record->{$this->idKey})
->execute();
}
+ // Make sure to update the new revision key for the entity.
+ $entity->{$this->revisionKey}->value = $record->{$this->revisionKey};
}
else {
+ // Remove the revision ID from the record to enable updates on SQL
+ // variants that prevent updating serial columns, for example,
+ // mssql.
+ unset($record->{$this->revisionKey});
$this->database
->update($this->revisionTable)
->fields((array) $record)
- ->condition($this->revisionKey, $record->{$this->revisionKey})
+ ->condition($this->revisionKey, $entity->getRevisionId())
->execute();
}
-
- // Make sure to update the new revision key for the entity.
- $entity->{$this->revisionKey}->value = $record->{$this->revisionKey};
-
- return $record->{$this->revisionKey};
+ return $entity->getRevisionId();
}
/**
@@ -1114,8 +1101,7 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
* @param array &$values
* An array of values keyed by entity ID.
* @param bool $load_from_revision
- * (optional) Flag to indicate whether revisions should be loaded or not,
- * defaults to FALSE.
+ * Flag to indicate whether revisions should be loaded or not.
*/
protected function loadFromDedicatedTables(array &$values, $load_from_revision) {
if (empty($values)) {
@@ -1167,21 +1153,22 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
foreach ($results as $row) {
$bundle = $row->bundle;
+ $value_key = !$load_from_revision ? $row->entity_id : $row->revision_id;
// Field values in default language are stored with
// LanguageInterface::LANGCODE_DEFAULT as key.
$langcode = LanguageInterface::LANGCODE_DEFAULT;
- if ($this->langcodeKey && isset($default_langcodes[$row->entity_id]) && $row->langcode != $default_langcodes[$row->entity_id]) {
+ if ($this->langcodeKey && isset($default_langcodes[$value_key]) && $row->langcode != $default_langcodes[$value_key]) {
$langcode = $row->langcode;
}
- if (!isset($values[$row->entity_id][$field_name][$langcode])) {
- $values[$row->entity_id][$field_name][$langcode] = [];
+ if (!isset($values[$value_key][$field_name][$langcode])) {
+ $values[$value_key][$field_name][$langcode] = [];
}
// Ensure that records for non-translatable fields having invalid
// languages are skipped.
if ($langcode == LanguageInterface::LANGCODE_DEFAULT || $definitions[$bundle][$field_name]->isTranslatable()) {
- if ($storage_definition->getCardinality() == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED || count($values[$row->entity_id][$field_name][$langcode]) < $storage_definition->getCardinality()) {
+ if ($storage_definition->getCardinality() == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED || count($values[$value_key][$field_name][$langcode]) < $storage_definition->getCardinality()) {
$item = [];
// For each column declared by the field, populate the item from the
// prefixed database column.
@@ -1192,7 +1179,7 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
}
// Add the item to the field values for the entity.
- $values[$row->entity_id][$field_name][$langcode][] = $item;
+ $values[$value_key][$field_name][$langcode][] = $item;
}
}
}
@@ -1438,14 +1425,6 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
* {@inheritdoc}
*/
public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition) {
- // If we are adding a field stored in a shared table we need to recompute
- // the table mapping.
- // @todo This does not belong here. Remove it once we are able to generate a
- // fresh table mapping in the schema handler. See
- // https://www.drupal.org/node/2274017.
- if ($this->getTableMapping()->allowsSharedTableStorage($storage_definition)) {
- $this->tableMapping = NULL;
- }
$this->wrapSchemaException(function () use ($storage_definition) {
$this->getStorageSchema()->onFieldStorageDefinitionCreate($storage_definition);
});
@@ -1468,9 +1447,7 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
$this->entityManager->getLastInstalledFieldStorageDefinitions($this->entityType->id())
);
- // @todo Remove the FieldStorageConfigInterface check when non-configurable
- // fields support purging: https://www.drupal.org/node/2282119.
- if ($storage_definition instanceof FieldStorageConfigInterface && $table_mapping->requiresDedicatedTableStorage($storage_definition)) {
+ if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
// Mark all data associated with the field for deletion.
$table = $table_mapping->getDedicatedDataTableName($storage_definition);
$revision_table = $table_mapping->getDedicatedRevisionTableName($storage_definition);
@@ -1554,9 +1531,8 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
// Check whether the whole field storage definition is gone, or just some
// bundle fields.
$storage_definition = $field_definition->getFieldStorageDefinition();
- $is_deleted = $this->storageDefinitionIsDeleted($storage_definition);
$table_mapping = $this->getTableMapping();
- $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $is_deleted);
+ $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $storage_definition->isDeleted());
// Get the entities which we want to purge first.
$entity_query = $this->database->select($table_name, 't', ['fetch' => \PDO::FETCH_ASSOC]);
@@ -1614,7 +1590,7 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
*/
protected function purgeFieldItems(ContentEntityInterface $entity, FieldDefinitionInterface $field_definition) {
$storage_definition = $field_definition->getFieldStorageDefinition();
- $is_deleted = $this->storageDefinitionIsDeleted($storage_definition);
+ $is_deleted = $storage_definition->isDeleted();
$table_mapping = $this->getTableMapping();
$table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $is_deleted);
$revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition, $is_deleted);
@@ -1651,7 +1627,7 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
$table_mapping = $this->getTableMapping($storage_definitions);
if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
- $is_deleted = $this->storageDefinitionIsDeleted($storage_definition);
+ $is_deleted = $storage_definition->isDeleted();
if ($this->entityType->isRevisionable()) {
$table_name = $table_mapping->getDedicatedRevisionTableName($storage_definition, $is_deleted);
}
@@ -1673,16 +1649,7 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
elseif ($table_mapping->allowsSharedTableStorage($storage_definition)) {
// Ascertain the table this field is mapped too.
$field_name = $storage_definition->getName();
- try {
- $table_name = $table_mapping->getFieldTableName($field_name);
- }
- catch (SqlContentEntityStorageException $e) {
- // This may happen when changing field storage schema, since we are not
- // able to use a table mapping matching the passed storage definition.
- // @todo Revisit this once we are able to instantiate the table mapping
- // properly. See https://www.drupal.org/node/2274017.
- $table_name = $this->dataTable ?: $this->baseTable;
- }
+ $table_name = $table_mapping->getFieldTableName($field_name);
$query = $this->database->select($table_name, 't');
$or = $query->orConditionGroup();
foreach (array_keys($storage_definition->getColumns()) as $property_name) {
@@ -1724,15 +1691,14 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
*
* @return bool
* Whether the field has been already deleted.
+ *
+ * @deprecated in Drupal 8.5.x, will be removed before Drupal 9.0.0. Use
+ * \Drupal\Core\Field\FieldStorageDefinitionInterface::isDeleted() instead.
+ *
+ * @see https://www.drupal.org/node/2907785
*/
protected function storageDefinitionIsDeleted(FieldStorageDefinitionInterface $storage_definition) {
- // Configurable fields are marked for deletion.
- if ($storage_definition instanceof FieldStorageConfigInterface) {
- return $storage_definition->isDeleted();
- }
- // For non configurable fields check whether they are still in the last
- // installed schema repository.
- return !array_key_exists($storage_definition->getName(), $this->entityManager->getLastInstalledFieldStorageDefinitions($this->entityTypeId));
+ return $storage_definition->isDeleted();
}
}
diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php
index d180381..f4505bd 100644
--- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php
+++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php
@@ -3,7 +3,7 @@
namespace Drupal\Core\Entity\Sql;
use Drupal\Core\Database\Connection;
-use Drupal\Core\Database\DatabaseException;
+use Drupal\Core\Database\DatabaseExceptionWrapper;
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\Core\Entity\EntityManagerInterface;
@@ -15,7 +15,7 @@ use Drupal\Core\Entity\Schema\DynamicallyFieldableEntityStorageSchemaInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Field\FieldException;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
-use Drupal\field\FieldStorageConfigInterface;
+use Drupal\Core\Language\LanguageInterface;
/**
* Defines a schema handler that supports revisionable, translatable entities.
@@ -87,6 +87,13 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
protected $installedStorageSchema;
/**
+ * The deleted fields repository.
+ *
+ * @var \Drupal\Core\Field\DeletedFieldsRepositoryInterface
+ */
+ protected $deletedFieldsRepository;
+
+ /**
* Constructs a SqlContentEntityStorageSchema.
*
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
@@ -123,6 +130,23 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
}
/**
+ * Gets the deleted fields repository.
+ *
+ * @return \Drupal\Core\Field\DeletedFieldsRepositoryInterface
+ * The deleted fields repository.
+ *
+ * @todo Inject this dependency in the constructor once this class can be
+ * instantiated as a regular entity handler:
+ * https://www.drupal.org/node/2332857.
+ */
+ protected function deletedFieldsRepository() {
+ if (!isset($this->deletedFieldsRepository)) {
+ $this->deletedFieldsRepository = \Drupal::service('entity_field.deleted_fields_repository');
+ }
+ return $this->deletedFieldsRepository;
+ }
+
+ /**
* {@inheritdoc}
*/
public function requiresEntityStorageSchemaChanges(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
@@ -204,8 +228,10 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
$current_schema = $this->getSchemaFromStorageDefinition($storage_definition);
$this->processFieldStorageSchema($current_schema);
+ $installed_schema = $this->loadFieldSchemaData($original);
+ $this->processFieldStorageSchema($installed_schema);
- return $current_schema != $this->loadFieldSchemaData($original);
+ return $current_schema != $installed_schema;
}
/**
@@ -219,7 +245,7 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
* The schema data.
*/
protected function getSchemaFromStorageDefinition(FieldStorageDefinitionInterface $storage_definition) {
- assert('!$storage_definition->hasCustomStorage();');
+ assert(!$storage_definition->hasCustomStorage());
$table_mapping = $this->storage->getTableMapping();
$schema = [];
if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
@@ -362,36 +388,21 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
public function onEntityTypeDelete(EntityTypeInterface $entity_type) {
$this->checkEntityType($entity_type);
$schema_handler = $this->database->schema();
- $actual_definition = $this->entityManager->getDefinition($entity_type->id());
- // @todo Instead of switching the wrapped entity type, we should be able to
- // instantiate a new table mapping for each entity type definition. See
- // https://www.drupal.org/node/2274017.
- $this->storage->setEntityType($entity_type);
-
- // Delete entity tables.
- foreach ($this->getEntitySchemaTables() as $table_name) {
+
+ $field_storage_definitions = $this->entityManager->getLastInstalledFieldStorageDefinitions($entity_type->id());
+ $table_mapping = $this->storage->getCustomTableMapping($entity_type, $field_storage_definitions);
+
+ // Delete entity and field tables.
+ foreach ($table_mapping->getTableNames() as $table_name) {
if ($schema_handler->tableExists($table_name)) {
$schema_handler->dropTable($table_name);
}
}
- // Delete dedicated field tables.
- $field_storage_definitions = $this->entityManager->getLastInstalledFieldStorageDefinitions($entity_type->id());
- $this->originalDefinitions = $field_storage_definitions;
- $table_mapping = $this->storage->getTableMapping($field_storage_definitions);
+ // Delete the field schema data.
foreach ($field_storage_definitions as $field_storage_definition) {
- // If we have a field having dedicated storage we need to drop it,
- // otherwise we just remove the related schema data.
- if ($table_mapping->requiresDedicatedTableStorage($field_storage_definition)) {
- $this->deleteDedicatedTableSchema($field_storage_definition);
- }
- elseif ($table_mapping->allowsSharedTableStorage($field_storage_definition)) {
- $this->deleteFieldSchemaData($field_storage_definition);
- }
+ $this->deleteFieldSchemaData($field_storage_definition);
}
- $this->originalDefinitions = NULL;
-
- $this->storage->setEntityType($actual_definition);
// Delete the entity schema.
$this->deleteEntitySchemaData($entity_type);
@@ -410,25 +421,17 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
// Store original definitions so that switching between shared and dedicated
// field table layout works.
- $this->originalDefinitions = $this->fieldStorageDefinitions;
- $this->originalDefinitions[$original->getName()] = $original;
$this->performFieldSchemaOperation('update', $storage_definition, $original);
- $this->originalDefinitions = NULL;
}
/**
* {@inheritdoc}
*/
public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $storage_definition) {
- // Only configurable fields currently support purging, so prevent deletion
- // of ones we can't purge if they have existing data.
- // @todo Add purging to all fields: https://www.drupal.org/node/2282119.
try {
- if (!($storage_definition instanceof FieldStorageConfigInterface) && $this->storage->countFieldData($storage_definition, TRUE)) {
- throw new FieldStorageDefinitionUpdateForbiddenException('Unable to delete a field (' . $storage_definition->getName() . ' in ' . $storage_definition->getTargetEntityTypeId() . ' entity) with data that cannot be purged.');
- }
+ $has_data = $this->storage->countFieldData($storage_definition, TRUE);
}
- catch (DatabaseException $e) {
+ catch (DatabaseExceptionWrapper $e) {
// This may happen when changing field storage schema, since we are not
// able to use a table mapping matching the passed storage definition.
// @todo Revisit this once we are able to instantiate the table mapping
@@ -436,10 +439,23 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
return;
}
+ // If the field storage does not have any data, we can safely delete its
+ // schema.
+ if (!$has_data) {
+ $this->performFieldSchemaOperation('delete', $storage_definition);
+ return;
+ }
+
+ // There's nothing else we can do if the field storage has a custom storage.
+ if ($storage_definition->hasCustomStorage()) {
+ return;
+ }
+
// Retrieve a table mapping which contains the deleted field still.
- $table_mapping = $this->storage->getTableMapping(
- $this->entityManager->getLastInstalledFieldStorageDefinitions($this->entityType->id())
- );
+ $storage_definitions = $this->entityManager->getLastInstalledFieldStorageDefinitions($this->entityType->id());
+ $table_mapping = $this->storage->getTableMapping($storage_definitions);
+ $field_table_name = $table_mapping->getFieldTableName($storage_definition->getName());
+
if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
// Move the table to a unique name while the table contents are being
// deleted.
@@ -452,11 +468,75 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
$this->database->schema()->renameTable($revision_table, $revision_new_table);
}
}
+ else {
+ // Move the field data from the shared table to a dedicated one in order
+ // to allow it to be purged like any other field.
+ $shared_table_field_columns = $table_mapping->getColumnNames($storage_definition->getName());
- // @todo Remove when finalizePurge() is invoked from the outside for all
- // fields: https://www.drupal.org/node/2282119.
- if (!($storage_definition instanceof FieldStorageConfigInterface)) {
- $this->performFieldSchemaOperation('delete', $storage_definition);
+ // Refresh the table mapping to use the deleted storage definition.
+ $deleted_storage_definition = $this->deletedFieldsRepository()->getFieldStorageDefinitions()[$storage_definition->getUniqueStorageIdentifier()];
+ $original_storage_definitions = [$storage_definition->getName() => $deleted_storage_definition] + $storage_definitions;
+ $table_mapping = $this->storage->getTableMapping($original_storage_definitions);
+
+ $dedicated_table_field_schema = $this->getDedicatedTableSchema($deleted_storage_definition);
+ $dedicated_table_field_columns = $table_mapping->getColumnNames($deleted_storage_definition->getName());
+
+ $dedicated_table_name = $table_mapping->getDedicatedDataTableName($deleted_storage_definition, TRUE);
+ $dedicated_table_name_mapping[$table_mapping->getDedicatedDataTableName($deleted_storage_definition)] = $dedicated_table_name;
+ if ($this->entityType->isRevisionable()) {
+ $dedicated_revision_table_name = $table_mapping->getDedicatedRevisionTableName($deleted_storage_definition, TRUE);
+ $dedicated_table_name_mapping[$table_mapping->getDedicatedRevisionTableName($deleted_storage_definition)] = $dedicated_revision_table_name;
+ }
+
+ // Create the dedicated field tables using "deleted" table names.
+ foreach ($dedicated_table_field_schema as $name => $table) {
+ if (!$this->database->schema()->tableExists($dedicated_table_name_mapping[$name])) {
+ $this->database->schema()->createTable($dedicated_table_name_mapping[$name], $table);
+ }
+ else {
+ throw new EntityStorageException('The field ' . $storage_definition->getName() . ' has already been deleted and it is in the process of being purged.');
+ }
+ }
+
+ if ($this->database->supportsTransactionalDDL()) {
+ // If the database supports transactional DDL, we can go ahead and rely
+ // on it. If not, we will have to rollback manually if something fails.
+ $transaction = $this->database->startTransaction();
+ }
+ try {
+ // Copy the data from the base table.
+ $this->database->insert($dedicated_table_name)
+ ->from($this->getSelectQueryForFieldStorageDeletion($field_table_name, $shared_table_field_columns, $dedicated_table_field_columns))
+ ->execute();
+
+ // Copy the data from the revision table.
+ if (isset($dedicated_revision_table_name)) {
+ if ($this->entityType->isTranslatable()) {
+ $revision_table = $storage_definition->isRevisionable() ? $this->storage->getRevisionDataTable() : $this->storage->getDataTable();
+ }
+ else {
+ $revision_table = $storage_definition->isRevisionable() ? $this->storage->getRevisionTable() : $this->storage->getBaseTable();
+ }
+ $this->database->insert($dedicated_revision_table_name)
+ ->from($this->getSelectQueryForFieldStorageDeletion($revision_table, $shared_table_field_columns, $dedicated_table_field_columns, $field_table_name))
+ ->execute();
+ }
+ }
+ catch (\Exception $e) {
+ if (isset($transaction)) {
+ $transaction->rollBack();
+ }
+ else {
+ // Delete the dedicated tables.
+ foreach ($dedicated_table_field_schema as $name => $table) {
+ $this->database->schema()->dropTable($dedicated_table_name_mapping[$name]);
+ }
+ }
+ throw $e;
+ }
+
+ // Delete the field from the shared tables.
+ $this->deleteSharedTableSchema($storage_definition);
}
}
@@ -468,6 +548,80 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
}
/**
+ * Returns a SELECT query suitable for inserting data into a dedicated table.
+ *
+ * @param string $table_name
+ * The entity table name to select from.
+ * @param array $shared_table_field_columns
+ * An array of field column names for a shared table schema.
+ * @param array $dedicated_table_field_columns
+ * An array of field column names for a dedicated table schema.
+ * @param string $base_table
+ * (optional) The name of the base entity table. Defaults to NULL.
+ *
+ * @return \Drupal\Core\Database\Query\SelectInterface
+ * A database select query.
+ */
+ protected function getSelectQueryForFieldStorageDeletion($table_name, array $shared_table_field_columns, array $dedicated_table_field_columns, $base_table = NULL) {
+ // Create a SELECT query that generates a result suitable for writing into
+ // a dedicated field table.
+ $select = $this->database->select($table_name, 'entity_table');
+
+ // Add the bundle column.
+ if ($bundle = $this->entityType->getKey('bundle')) {
+ if ($base_table) {
+ $select->join($base_table, 'base_table', "entity_table.{$this->entityType->getKey('id')} = %alias.{$this->entityType->getKey('id')}");
+ $select->addField('base_table', $bundle, 'bundle');
+ }
+ else {
+ $select->addField('entity_table', $bundle, 'bundle');
+ }
+ }
+ else {
+ $select->addExpression(':bundle', 'bundle', [':bundle' => $this->entityType->id()]);
+ }
+
+ // Add the deleted column.
+ $select->addExpression(':deleted', 'deleted', [':deleted' => 1]);
+
+ // Add the entity_id column.
+ $select->addField('entity_table', $this->entityType->getKey('id'), 'entity_id');
+
+ // Add the revision_id column.
+ if ($this->entityType->isRevisionable()) {
+ $select->addField('entity_table', $this->entityType->getKey('revision'), 'revision_id');
+ }
+ else {
+ $select->addField('entity_table', $this->entityType->getKey('id'), 'revision_id');
+ }
+
+ // Add the langcode column.
+ if ($langcode = $this->entityType->getKey('langcode')) {
+ $select->addField('entity_table', $langcode, 'langcode');
+ }
+ else {
+ $select->addExpression(':langcode', 'langcode', [':langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED]);
+ }
+
+ // Add the delta column and set it to 0 because we are only dealing with
+ // single cardinality fields.
+ $select->addExpression(':delta', 'delta', [':delta' => 0]);
+
+ // Add all the dynamic field columns.
+ $or = $select->orConditionGroup();
+ foreach ($shared_table_field_columns as $field_column_name => $schema_column_name) {
+ $select->addField('entity_table', $schema_column_name, $dedicated_table_field_columns[$field_column_name]);
+ $or->isNotNull('entity_table.' . $schema_column_name);
+ }
+ $select->condition($or);
+
+ // Lock the table rows.
+ $select->forUpdate(TRUE);
+
+ return $select;
+ }
+
+ /**
* Checks that we are dealing with the correct entity type.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
@@ -511,13 +665,6 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
$entity_type_id = $entity_type->id();
if (!isset($this->schema[$entity_type_id]) || $reset) {
- // Back up the storage definition and replace it with the passed one.
- // @todo Instead of switching the wrapped entity type, we should be able
- // to instantiate a new table mapping for each entity type definition.
- // See https://www.drupal.org/node/2274017.
- $actual_definition = $this->entityManager->getDefinition($entity_type_id);
- $this->storage->setEntityType($entity_type);
-
// Prepare basic information about the entity type.
$tables = $this->getEntitySchemaTables();
@@ -534,7 +681,7 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
}
// We need to act only on shared entity schema tables.
- $table_mapping = $this->storage->getTableMapping();
+ $table_mapping = $this->storage->getCustomTableMapping($entity_type, $this->fieldStorageDefinitions);
$table_names = array_diff($table_mapping->getTableNames(), $table_mapping->getDedicatedTableNames());
foreach ($table_names as $table_name) {
if (!isset($schema[$table_name])) {
@@ -584,9 +731,6 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
}
$this->schema[$entity_type_id] = $schema;
-
- // Restore the actual definition.
- $this->storage->