From 0297921b0527f4826b340615aa1088d732e0821b Mon Sep 17 00:00:00 2001 From: roadiz-ci Date: Fri, 14 Jun 2024 10:49:08 +0000 Subject: [PATCH] Merge branch hotfix/v2.3.10 --- .env | 1 + .env.test | 6 + .github/workflows/run-test.yml | 2 +- .travis.yml | 14 - LICENSE.md | 2 +- composer.json | 105 ++--- config/api_resources/attribute.yml | 9 +- config/api_resources/attribute_value.yml | 29 +- config/api_resources/common_content.yml | 29 ++ config/api_resources/custom_form.yml | 315 +++++-------- config/api_resources/document.yml | 30 +- config/api_resources/folder.yml | 24 +- config/api_resources/node.yml | 18 +- config/api_resources/nodes_sources.yml | 121 ++--- config/api_resources/realm.yml | 25 +- config/api_resources/tag.yml | 38 +- config/api_resources/translation.yml | 26 +- config/api_resources/web_response.yml | 42 +- config/fixtures/settings.json | 37 -- config/packages/api_platform.yaml | 49 +- config/packages/doctrine.yaml | 96 ++-- config/packages/roadiz_core.yaml | 3 - config/packages/security.yaml | 34 +- config/routing.yaml | 8 +- config/services.yaml | 44 +- migrations/Version20201203004857.php | 359 ++++++++++++++- migrations/Version20201225181256.php | 348 +------------- migrations/Version20230607134403.php | 80 ++++ migrations/Version20230615122615.php | 38 ++ migrations/Version20230628143106.php | 74 +++ migrations/Version20230628170203.php | 40 ++ migrations/Version20230712171650.php | 36 ++ migrations/Version20230828092821.php | 46 ++ migrations/Version20230829082257.php | 38 ++ migrations/Version20230905140844.php | 46 ++ migrations/Version20230915134833.php | 36 ++ migrations/Version20231012154717.php | 36 ++ migrations/Version20231013132932.php | 38 ++ migrations/Version20240214143213.php | 31 ++ migrations/Version20240214143849.php | 31 ++ migrations/Version20240214145403.php | 31 ++ migrations/Version20240305124809.php | 31 ++ migrations/Version20240305125653.php | 31 ++ migrations/Version20240305132609.php | 31 ++ migrations/Version20240305133122.php | 31 ++ migrations/Version20240305133641.php | 31 ++ migrations/Version20240305134734.php | 37 ++ migrations/Version20240305142243.php | 62 +++ migrations/Version20240305143443.php | 31 ++ migrations/Version20240318184555.php | 34 ++ migrations/Version20240318184556.php | 68 +++ migrations/Version20240318204224.php | 44 ++ migrations/Version20240603210209.php | 33 ++ migrations/Version20240604143759.php | 33 ++ phpstan.neon | 13 +- phpunit.xml.dist | 67 +++ .../NodesSourcesBreadcrumbsFactory.php | 2 - .../GetWebResponseByPathController.php | 113 +++-- .../TranslationAwareControllerTrait.php | 11 +- .../AttributeOutputDataTransformer.php | 45 -- .../AttributeValueOutputDataTransformer.php | 48 -- .../BaseNodesSourcesOutputDataTransformer.php | 35 -- ...eWebResponseOutputDataTransformerTrait.php | 15 +- .../CustomFormOutputDataTransformer.php | 59 --- .../DocumentOutputDataTransformer.php | 96 ---- .../FolderOutputDataTransformer.php | 49 -- .../NodeOutputDataTransformer.php | 39 -- .../NodesSourcesOutputDataTransformer.php | 52 --- .../TagOutputDataTransformer.php | 53 --- .../TranslationOutputDataTransformer.php | 36 -- .../WebResponseDataTransformerInterface.php | 18 +- .../WebResponseOutputDataTransformer.php | 97 ++-- src/Api/Dto/AttributeOutput.php | 51 --- src/Api/Dto/AttributeValueOutput.php | 41 -- src/Api/Dto/CustomFormOutput.php | 49 -- src/Api/Dto/DocumentOutput.php | 109 ----- src/Api/Dto/FolderOutput.php | 34 -- src/Api/Dto/NodeOutput.php | 34 -- src/Api/Dto/NodesSourcesDto.php | 59 --- src/Api/Dto/NodesSourcesOutput.php | 12 - src/Api/Dto/TagOutput.php | 57 --- src/Api/Dto/TranslationOutput.php | 34 -- src/Api/Extension/ArchiveExtension.php | 54 +-- .../AttributeValueQueryExtension.php | 84 ++++ .../AttributeValueRealmExtension.php | 69 +++ src/Api/Extension/DocumentQueryExtension.php | 19 +- src/Api/Extension/NodeQueryExtension.php | 21 +- .../Extension/NodesSourcesQueryExtension.php | 21 +- src/Api/Extension/NodesTagsQueryExtension.php | 89 ++++ src/Api/Filter/ArchiveFilter.php | 32 +- src/Api/Filter/CopyrightValidFilter.php | 12 +- src/Api/Filter/GeneratedEntityFilter.php | 18 +- src/Api/Filter/IntersectionFilter.php | 12 +- src/Api/Filter/LocaleFilter.php | 30 +- src/Api/Filter/NotFilter.php | 33 +- src/Api/ListManager/SolrPaginator.php | 6 +- src/Api/ListManager/SolrSearchListManager.php | 36 +- .../Model/BlocksAwareWebResponseInterface.php | 8 +- src/Api/Model/NodesSourcesHead.php | 52 +-- src/Api/Model/NodesSourcesHeadFactory.php | 23 +- .../Model/RealmsAwareWebResponseInterface.php | 8 +- src/Api/Model/WebResponse.php | 95 +--- src/Api/Model/WebResponseInterface.php | 18 + src/Api/Model/WebResponseTrait.php | 149 ++++++ src/Api/OpenApi/WebResponseDecorator.php | 4 + .../DefinitionFactoryConfiguration.php | 27 ++ .../Definition/DefinitionFactoryInterface.php | 12 + .../NonReachableNodeSourceBlockDefinition.php | 2 + .../ReachableNodeSourceDefinition.php | 2 + src/Api/TreeWalker/TreeWalkerGenerator.php | 74 ++- src/Bag/NodeTypes.php | 12 +- src/Bag/Roles.php | 19 +- src/Bag/Settings.php | 23 +- src/Cache/Clearer/FileClearer.php | 4 +- .../Clearer/NodesSourcesUrlsCacheClearer.php | 5 +- src/Cache/CloudflareProxyCache.php | 35 +- src/Cache/ReverseProxyCache.php | 28 +- src/Cache/ReverseProxyCacheLocator.php | 14 +- src/Console/AppInstallCommand.php | 168 +++++++ src/Console/AppMigrateCommand.php | 126 +++++ src/Console/CleanLoginAttemptCommand.php | 43 -- src/Console/CustomFormAnswerPurgeCommand.php | 13 +- src/Console/DecodePrivateKeyCommand.php | 49 -- src/Console/EncodePrivateKeyCommand.php | 50 -- src/Console/FilesCommandTrait.php | 6 +- src/Console/FilesExportCommand.php | 19 +- src/Console/FilesImportCommand.php | 21 +- src/Console/GenerateApiResourceCommand.php | 14 +- .../GenerateNodeSourceEntitiesCommand.php | 29 +- src/Console/GeneratePrivateKeyCommand.php | 47 -- src/Console/GetCronLastExecDateCommand.php | 43 ++ src/Console/InstallCommand.php | 61 +-- src/Console/LogsCleanupCommand.php | 25 +- src/Console/MailerTestCommand.php | 17 +- .../NodeApplyUniversalFieldsCommand.php | 20 +- src/Console/NodeClearTagCommand.php | 29 +- src/Console/NodeTypesCommand.php | 19 +- src/Console/NodeTypesCreationCommand.php | 25 +- src/Console/NodeTypesDefaultValuesCommand.php | 15 +- src/Console/NodeTypesDeleteCommand.php | 24 +- src/Console/NodesCleanNamesCommand.php | 19 +- src/Console/NodesCommand.php | 19 +- src/Console/NodesCreationCommand.php | 133 ------ src/Console/NodesDetailsCommand.php | 18 +- src/Console/NodesEmptyTrashCommand.php | 20 +- src/Console/NodesOrphansCommand.php | 19 +- src/Console/PrivateKeyCommand.php | 57 --- src/Console/PurgeLoginAttemptCommand.php | 54 --- .../RegisterCronLastExecDateCommand.php | 46 ++ src/Console/SolrCommand.php | 18 +- src/Console/SolrOptimizeCommand.php | 21 +- src/Console/SolrReindexCommand.php | 17 +- src/Console/SolrResetCommand.php | 21 +- src/Console/TranslationsCommand.php | 17 +- src/Console/TranslationsCreationCommand.php | 15 +- src/Console/TranslationsDeleteCommand.php | 15 +- src/Console/TranslationsDisableCommand.php | 15 +- src/Console/TranslationsEnableCommand.php | 15 +- src/Console/UsersCommand.php | 24 +- src/Console/UsersCreationCommand.php | 46 +- src/Console/UsersDeleteCommand.php | 46 +- src/Console/UsersDisableCommand.php | 46 +- src/Console/UsersEnableCommand.php | 45 +- src/Console/UsersPasswordCommand.php | 64 +-- src/Console/UsersRolesCommand.php | 88 ++-- src/Console/VersionsPurgeCommand.php | 14 +- src/Controller/CustomFormController.php | 260 ++--------- src/Controller/RedirectionController.php | 3 - src/Crypto/UniqueKeyEncoderFactory.php | 55 --- src/CustomForm/CustomFormHelper.php | 2 +- .../Message/CustomFormAnswerNotifyMessage.php | 49 ++ .../CustomFormAnswerNotifyMessageHandler.php | 124 +++++ .../Compiler/MediaFinderCompilerPass.php | 1 + .../NodesSourcesEntitiesPathCompilerPass.php | 3 + ...reeWalkerDefinitionFactoryCompilerPass.php | 43 ++ src/DependencyInjection/Configuration.php | 24 +- .../RoadizCoreExtension.php | 52 +-- .../Event/FilterQueryBuilderEvent.php | 6 +- src/Doctrine/Event/QueryEvent.php | 4 +- src/Doctrine/Event/QueryNodesSourcesEvent.php | 4 +- .../AttributeValueLifeCycleSubscriber.php | 7 + .../SettingLifeCycleSubscriber.php | 123 ----- .../UserLifeCycleSubscriber.php | 37 +- src/Doctrine/ORM/Filter/ANodesFilter.php | 50 +- src/Doctrine/ORM/Filter/BNodesFilter.php | 8 - src/Doctrine/ORM/SimpleQueryBuilder.php | 12 +- src/Doctrine/SchemaUpdater.php | 18 +- .../DocumentMessageDispatchSubscriber.php | 19 +- .../DocumentAudioVideoMessageHandler.php | 9 + .../DocumentPdfMessageHandler.php | 9 + src/Entity/AbstractDateTimedPositioned.php | 42 ++ src/Entity/Attribute.php | 61 ++- src/Entity/AttributeDocuments.php | 44 +- src/Entity/AttributeValue.php | 79 ++-- src/Entity/CustomForm.php | 23 +- src/Entity/CustomFormAnswer.php | 13 +- src/Entity/CustomFormField.php | 23 +- src/Entity/CustomFormFieldAttribute.php | 33 +- src/Entity/Document.php | 108 ++++- src/Entity/DocumentTranslation.php | 22 +- src/Entity/FieldAwareEntityTrait.php | 44 ++ src/Entity/Folder.php | 8 +- src/Entity/FolderTranslation.php | 16 +- src/Entity/Group.php | 2 +- src/Entity/LoginAttempt.php | 106 ----- src/Entity/Node.php | 168 ++++--- src/Entity/NodeType.php | 71 ++- src/Entity/NodeTypeField.php | 76 +--- src/Entity/NodesCustomForms.php | 65 +-- src/Entity/NodesSources.php | 203 +++++---- src/Entity/NodesSourcesDocuments.php | 77 +--- src/Entity/NodesToNodes.php | 85 +--- src/Entity/Realm.php | 17 +- src/Entity/RealmNode.php | 14 +- src/Entity/Redirection.php | 37 +- src/Entity/Role.php | 2 +- src/Entity/Setting.php | 92 +--- src/Entity/SettingGroup.php | 2 +- src/Entity/Tag.php | 58 ++- src/Entity/TagTranslation.php | 84 +--- src/Entity/TagTranslationDocuments.php | 40 +- src/Entity/Theme.php | 19 +- src/Entity/Translation.php | 36 +- src/Entity/UrlAlias.php | 28 +- src/Entity/User.php | 51 +-- src/Entity/Webhook.php | 1 + src/EntityApi/AbstractApi.php | 2 - src/EntityApi/NodeSourceApi.php | 54 +-- src/EntityHandler/CustomFormFieldHandler.php | 29 +- src/EntityHandler/CustomFormHandler.php | 13 +- src/EntityHandler/DocumentHandler.php | 28 +- src/EntityHandler/FolderHandler.php | 52 +-- src/EntityHandler/GroupHandler.php | 2 +- src/EntityHandler/HandlerFactory.php | 10 +- src/EntityHandler/NodeHandler.php | 106 ++--- src/EntityHandler/NodeTypeFieldHandler.php | 12 +- src/EntityHandler/NodeTypeHandler.php | 49 +- src/EntityHandler/NodesSourcesHandler.php | 69 ++- src/EntityHandler/TagHandler.php | 135 +----- src/EntityHandler/TranslationHandler.php | 17 +- .../NodesSourcesPathGeneratingEvent.php | 56 +-- .../PostCreatedRedirectionEvent.php | 9 + .../PostDeletedRedirectionEvent.php | 9 + .../PostUpdatedRedirectionEvent.php | 9 + src/Event/Redirection/RedirectionEvent.php | 39 ++ .../AssetsCacheEventSubscriber.php | 17 +- .../AttributeValueIndexingSubscriber.php | 90 ---- .../AutomaticWebhookSubscriber.php | 30 +- .../CloudflareCacheEventSubscriber.php | 45 +- src/EventSubscriber/LocaleSubscriber.php | 104 ++++- src/EventSubscriber/LoggableSubscriber.php | 23 +- .../NodeDuplicationSubscriber.php | 18 +- src/EventSubscriber/NodeNameSubscriber.php | 1 - .../NodeRedirectionSubscriber.php | 22 +- .../NodeSourcePathSubscriber.php | 17 +- .../NodesSourcesAddHeadersSubscriber.php | 86 ++++ .../NodesSourcesLinkHeaderEventSubscriber.php | 67 +-- .../NodesSourcesUniversalSubscriber.php | 1 - .../NodesSourcesUrlsCacheEventSubscriber.php | 11 - .../OPCacheEventSubscriber.php | 1 - .../RealmNodeInheritanceSubscriber.php | 4 +- .../RedirectionCacheSubscriber.php | 40 ++ .../ReverseProxyCacheEventSubscriber.php | 11 +- src/EventSubscriber/RoleSubscriber.php | 3 - src/EventSubscriber/SignatureSubscriber.php | 18 +- .../TagTimestampSubscriber.php | 5 +- src/EventSubscriber/TranslationSubscriber.php | 10 +- src/EventSubscriber/UserLocaleSubscriber.php | 21 +- src/Exception/EmptySaltException.php | 12 - src/Exception/ExceptionViewer.php | 326 ------------- .../TooManyLoginAttemptsException.php | 18 - src/Form/AttributeChoiceType.php | 14 +- src/Form/AttributeType.php | 13 + src/Form/AttributeValueRealmType.php | 30 ++ src/Form/AttributeValueTranslationType.php | 14 + .../Constraint/NodeTypeFieldValidator.php | 50 +- src/Form/Constraint/UniqueEntity.php | 42 -- src/Form/Constraint/UniqueEntityValidator.php | 171 ------- src/Form/CustomFormsType.php | 9 +- src/Form/DataListTextType.php | 43 ++ .../ExplorerProviderItemTransformer.php | 2 +- .../DataTransformer/JoinDataTransformer.php | 3 +- src/Form/RoleEntityType.php | 2 +- src/Form/SettingType.php | 4 - src/Form/ThemesType.php | 4 +- src/Importer/NodeTypesImporter.php | 25 +- src/ListManager/AbstractEntityListManager.php | 68 +++ src/ListManager/EntityListManager.php | 72 +-- src/ListManager/NodePaginator.php | 6 +- src/ListManager/NodesSourcesPaginator.php | 2 +- src/ListManager/QueryBuilderListManager.php | 71 ++- src/ListManager/SessionListFilters.php | 53 +++ src/Logger/DoctrineHandler.php | 206 ++++++--- src/{ => Logger}/Entity/Log.php | 139 ++++-- src/Mailer/ContactFormManager.php | 48 +- src/Mailer/EmailManager.php | 67 +-- .../ApplyRealmNodeInheritanceMessage.php | 14 +- .../CleanRealmNodeInheritanceMessage.php | 14 +- src/Message/DeleteNodeTypeMessage.php | 10 +- ...pplyRealmNodeInheritanceMessageHandler.php | 16 +- ...leanRealmNodeInheritanceMessageHandler.php | 12 +- .../Handler/DeleteNodeTypeMessageHandler.php | 14 +- .../Handler/HttpRequestMessageHandler.php | 18 +- .../PurgeReverseProxyCacheMessageHandler.php | 23 +- ...archRealmNodeInheritanceMessageHandler.php | 21 +- .../UpdateDoctrineSchemaMessageHandler.php | 5 +- .../UpdateNodeTypeSchemaMessageHandler.php | 14 +- src/Message/PurgeReverseProxyCacheMessage.php | 10 +- .../SearchRealmNodeInheritanceMessage.php | 8 +- src/Message/UpdateNodeTypeSchemaMessage.php | 10 +- src/Model/AttributeGroupTrait.php | 14 +- src/Model/AttributeGroupTranslationTrait.php | 26 +- src/Model/AttributeInterface.php | 5 + src/Model/AttributeTrait.php | 44 +- src/Model/AttributeTranslationTrait.php | 23 +- src/Model/AttributeValueInterface.php | 5 +- src/Model/AttributeValueTrait.php | 48 +- src/Model/AttributeValueTranslationTrait.php | 58 +-- src/Model/RealmInterface.php | 3 +- src/Node/NodeDuplicator.php | 15 +- src/Node/NodeFactory.php | 27 +- src/Node/NodeMover.php | 6 +- src/Node/NodeNameChecker.php | 2 +- src/Node/NodeTranstyper.php | 36 +- src/Node/UniqueNodeGenerator.php | 46 +- src/Node/UniversalDataDuplicator.php | 19 +- src/NodeType/ApiResourceGenerator.php | 309 ++++++++++--- .../ApiResourceOperationNameGenerator.php | 28 ++ src/NodeType/DefaultValuesResolver.php | 5 +- .../EventSubscriber/PreviewBarSubscriber.php | 10 +- .../EventSubscriber/PreviewModeSubscriber.php | 37 +- .../Exception/PreviewNotAllowedException.php | 2 +- src/Preview/RequestPreviewRevolver.php | 2 +- src/Preview/User/PreviewUserProvider.php | 2 +- src/Realm/RealmResolver.php | 79 +++- src/Realm/RealmResolverInterface.php | 23 + src/Repository/AttributeValueRepository.php | 21 +- src/Repository/CustomFormAnswerRepository.php | 2 +- src/Repository/CustomFormRepository.php | 28 +- src/Repository/DocumentRepository.php | 32 +- src/Repository/EntityRepository.php | 23 +- src/Repository/LogRepository.php | 60 ++- src/Repository/LoginAttemptRepository.php | 139 ------ src/Repository/NodeRepository.php | 160 ++++++- src/Repository/NodesCustomFormsRepository.php | 35 +- .../NodesSourcesDocumentsRepository.php | 27 +- src/Repository/NodesSourcesRepository.php | 429 ++++++++++++++++-- src/Repository/NodesToNodesRepository.php | 30 +- src/Repository/PrefixAwareRepository.php | 8 +- src/Repository/RealmNodeRepository.php | 2 +- src/Repository/RealmRepository.php | 9 + src/Repository/StatusAwareRepository.php | 4 +- src/Repository/TagRepository.php | 32 +- src/Repository/UrlAliasRepository.php | 19 +- src/Repository/UserLogEntryRepository.php | 1 - src/RoadizCoreBundle.php | 2 + src/Routing/DeferredRouteCollection.php | 29 -- src/Routing/DocumentUrlGenerator.php | 2 +- src/Routing/DynamicUrlMatcher.php | 20 +- src/Routing/InstallRouteCollection.php | 36 -- src/Routing/NodePathInfo.php | 6 +- src/Routing/NodeRouteHelper.php | 63 ++- src/Routing/NodeRouter.php | 79 +--- src/Routing/NodeUrlMatcher.php | 47 +- src/Routing/NodesSourcesPathResolver.php | 32 +- src/Routing/NodesSourcesUrlGenerator.php | 27 +- src/Routing/NullLoader.php | 28 +- ...timizedNodesSourcesGraphPathAggregator.php | 25 +- src/Routing/RedirectableUrlMatcher.php | 2 +- src/Routing/RedirectionMatcher.php | 29 +- src/Routing/RedirectionPathResolver.php | 54 ++- src/Routing/RedirectionRouter.php | 27 +- src/Routing/StaticRouter.php | 52 --- src/SearchEngine/AbstractSearchHandler.php | 72 ++- src/SearchEngine/ClientRegistry.php | 9 +- src/SearchEngine/DocumentSearchHandler.php | 64 +-- .../Event/AbstractSearchQueryEvent.php | 36 ++ .../Event/DocumentSearchQueryEvent.php | 9 + .../Event/NodeSourceSearchQueryEvent.php | 9 + .../GlobalNodeSourceSearchHandler.php | 69 +-- src/SearchEngine/Indexer/AbstractIndexer.php | 14 +- src/SearchEngine/Indexer/DocumentIndexer.php | 13 +- .../Indexer/NodesSourcesIndexer.php | 13 +- .../Message/AbstractSolrMessage.php | 7 +- .../Handler/SolrDeleteMessageHandler.php | 2 + .../Handler/SolrReindexMessageHandler.php | 2 + src/SearchEngine/NodeSourceSearchHandler.php | 94 ++-- src/SearchEngine/SearchHandlerInterface.php | 4 - src/SearchEngine/SearchResultsInterface.php | 6 + .../SolariumDocumentTranslation.php | 7 +- src/SearchEngine/SolariumLogger.php | 187 ++++++++ src/SearchEngine/SolariumNodeSource.php | 5 +- src/SearchEngine/SolrSearchResultItem.php | 48 ++ src/SearchEngine/SolrSearchResults.php | 101 ++--- .../Subscriber/AbstractIndexingSubscriber.php | 53 ++- .../AttributeValueIndexingSubscriber.php | 125 +++++ ...tDocumentTranslationIndexingSubscriber.php | 2 +- .../DefaultNodesSourcesIndexingSubscriber.php | 125 +++-- .../TreeWalkerIndexingEventSubscriber.php | 100 ++++ .../Manager/LoginAttemptManager.php | 160 ------- .../Authentication/RoadizAuthenticator.php | 4 +- .../Authorization/AccessDeniedHandler.php | 2 +- .../Authorization/Voter/GroupVoter.php | 12 +- .../Voter/NodeTypeFieldVoter.php | 4 +- .../Authorization/Voter/NodeVoter.php | 25 +- .../Authorization/Voter/RealmVoter.php | 44 +- .../Authorization/Voter/RoleArrayVoter.php | 6 +- .../Voter/SuperAdminRoleHierarchyVoter.php | 14 +- src/Security/User/UserViewer.php | 47 +- src/Serializer/CircularReferenceHandler.php | 24 +- .../Normalizer/AbstractPathNormalizer.php | 24 +- .../Normalizer/AttributeValueNormalizer.php | 14 +- .../Normalizer/CustomFormNormalizer.php | 3 +- .../Normalizer/DocumentNormalizer.php | 20 +- .../Normalizer/DocumentSourcesNormalizer.php | 7 +- .../Normalizer/FolderNormalizer.php | 2 +- .../Normalizer/NodesSourcesPathNormalizer.php | 2 +- .../RealmSerializationGroupNormalizer.php | 50 +- src/Serializer/Normalizer/TagNormalizer.php | 33 +- .../Normalizer/TranslationAwareNormalizer.php | 54 ++- .../AbstractTypedObjectConstructor.php | 17 +- .../AttributeObjectConstructor.php | 55 +++ .../ChainDoctrineObjectConstructor.php | 42 +- .../GroupObjectConstructor.php | 4 +- .../NodeObjectConstructor.php | 4 +- .../NodeTypeFieldObjectConstructor.php | 64 --- .../NodeTypeObjectConstructor.php | 8 +- .../RoleObjectConstructor.php | 4 +- .../SettingGroupObjectConstructor.php | 4 +- .../SettingObjectConstructor.php | 4 +- .../TagObjectConstructor.php | 4 +- .../TranslationObjectConstructor.php | 11 +- .../TranslationAwareContextBuilder.php | 64 +-- .../NodeType/ApiResourceGeneratorTest.php | 152 +++++++ src/Traits/LoginRequestTrait.php | 15 +- src/TwigExtension/AttributesExtension.php | 14 +- src/TwigExtension/BlockRenderExtension.php | 12 +- .../CentralTruncateExtension.php | 2 +- src/TwigExtension/DocumentUrlExtension.php | 17 +- src/TwigExtension/HandlerExtension.php | 14 +- src/TwigExtension/JwtExtension.php | 14 +- src/TwigExtension/LogExtension.php | 114 +++++ src/TwigExtension/NodesSourcesExtension.php | 117 ++--- src/TwigExtension/RoadizExtension.php | 35 +- src/TwigExtension/RoutingExtension.php | 19 +- src/TwigExtension/TransChoiceExtension.php | 8 +- src/TwigExtension/TranslationExtension.php | 2 +- .../TranslationMenuExtension.php | 17 +- .../Event/NodeStatusGuardListener.php | 11 +- src/Xlsx/NodeSourceXlsxSerializer.php | 1 - src/Xlsx/XlsxExporter.php | 14 +- templates/DataCollector/solarium.html.twig | 81 ++++ templates/DataCollector/solr.svg | 12 + templates/customForm/customForm.html.twig | 2 +- templates/email/forms/answerForm.html.twig | 7 +- templates/email/forms/answerForm.txt.twig | 6 +- tests/bootstrap.php | 11 + tests/expected_api_resources/nssecondtest.yml | 35 ++ tests/expected_api_resources/nstest.yml | 35 ++ tests/expected_api_resources/web_response.yml | 32 ++ .../web_response_multiple.yml | 61 +++ tests/object-manager.php | 11 + translations/core/messages.en.xlf | 18 + translations/core/messages.fr.xlf | 18 + translations/core/messages.xlf | 18 + translations/security.ar.xlf | 11 + translations/security.de.xlf | 11 + translations/security.en.xlf | 16 +- translations/security.es.xlf | 11 + translations/security.fr.xlf | 18 +- translations/security.id.xlf | 11 + translations/security.it.xlf | 11 + translations/security.ru.xlf | 11 + translations/security.sr.xlf | 11 + translations/security.tr.xlf | 11 + translations/security.uk.xlf | 11 + translations/security.zh.xlf | 11 + translations/validators.ar.xlf | 1 + translations/validators.en.xlf | 4 + translations/validators.es.xlf | 1 + translations/validators.fr.xlf | 4 + translations/validators.id.xlf | 1 + translations/validators.it.xlf | 1 + translations/validators.ru.xlf | 1 + translations/validators.sr.xlf | 1 + translations/validators.tr.xlf | 1 + translations/validators.xlf | 4 + 487 files changed, 9783 insertions(+), 8797 deletions(-) create mode 100644 .env create mode 100644 .env.test delete mode 100644 .travis.yml create mode 100644 config/api_resources/common_content.yml create mode 100644 migrations/Version20230607134403.php create mode 100644 migrations/Version20230615122615.php create mode 100644 migrations/Version20230628143106.php create mode 100644 migrations/Version20230628170203.php create mode 100644 migrations/Version20230712171650.php create mode 100644 migrations/Version20230828092821.php create mode 100644 migrations/Version20230829082257.php create mode 100644 migrations/Version20230905140844.php create mode 100644 migrations/Version20230915134833.php create mode 100644 migrations/Version20231012154717.php create mode 100644 migrations/Version20231013132932.php create mode 100644 migrations/Version20240214143213.php create mode 100644 migrations/Version20240214143849.php create mode 100644 migrations/Version20240214145403.php create mode 100644 migrations/Version20240305124809.php create mode 100644 migrations/Version20240305125653.php create mode 100644 migrations/Version20240305132609.php create mode 100644 migrations/Version20240305133122.php create mode 100644 migrations/Version20240305133641.php create mode 100644 migrations/Version20240305134734.php create mode 100644 migrations/Version20240305142243.php create mode 100644 migrations/Version20240305143443.php create mode 100644 migrations/Version20240318184555.php create mode 100644 migrations/Version20240318184556.php create mode 100644 migrations/Version20240318204224.php create mode 100644 migrations/Version20240603210209.php create mode 100644 migrations/Version20240604143759.php create mode 100644 phpunit.xml.dist delete mode 100644 src/Api/DataTransformer/AttributeOutputDataTransformer.php delete mode 100644 src/Api/DataTransformer/AttributeValueOutputDataTransformer.php delete mode 100644 src/Api/DataTransformer/BaseNodesSourcesOutputDataTransformer.php delete mode 100644 src/Api/DataTransformer/CustomFormOutputDataTransformer.php delete mode 100644 src/Api/DataTransformer/DocumentOutputDataTransformer.php delete mode 100644 src/Api/DataTransformer/FolderOutputDataTransformer.php delete mode 100644 src/Api/DataTransformer/NodeOutputDataTransformer.php delete mode 100644 src/Api/DataTransformer/NodesSourcesOutputDataTransformer.php delete mode 100644 src/Api/DataTransformer/TagOutputDataTransformer.php delete mode 100644 src/Api/DataTransformer/TranslationOutputDataTransformer.php delete mode 100644 src/Api/Dto/AttributeOutput.php delete mode 100644 src/Api/Dto/AttributeValueOutput.php delete mode 100644 src/Api/Dto/CustomFormOutput.php delete mode 100644 src/Api/Dto/DocumentOutput.php delete mode 100644 src/Api/Dto/FolderOutput.php delete mode 100644 src/Api/Dto/NodeOutput.php delete mode 100644 src/Api/Dto/NodesSourcesDto.php delete mode 100644 src/Api/Dto/NodesSourcesOutput.php delete mode 100644 src/Api/Dto/TagOutput.php delete mode 100644 src/Api/Dto/TranslationOutput.php create mode 100644 src/Api/Extension/AttributeValueQueryExtension.php create mode 100644 src/Api/Extension/AttributeValueRealmExtension.php create mode 100644 src/Api/Extension/NodesTagsQueryExtension.php create mode 100644 src/Api/Model/WebResponseTrait.php create mode 100644 src/Api/TreeWalker/Definition/DefinitionFactoryConfiguration.php create mode 100644 src/Api/TreeWalker/Definition/DefinitionFactoryInterface.php create mode 100644 src/Console/AppInstallCommand.php create mode 100644 src/Console/AppMigrateCommand.php delete mode 100644 src/Console/CleanLoginAttemptCommand.php delete mode 100644 src/Console/DecodePrivateKeyCommand.php delete mode 100644 src/Console/EncodePrivateKeyCommand.php delete mode 100644 src/Console/GeneratePrivateKeyCommand.php create mode 100644 src/Console/GetCronLastExecDateCommand.php delete mode 100644 src/Console/NodesCreationCommand.php delete mode 100644 src/Console/PrivateKeyCommand.php delete mode 100644 src/Console/PurgeLoginAttemptCommand.php create mode 100644 src/Console/RegisterCronLastExecDateCommand.php delete mode 100644 src/Crypto/UniqueKeyEncoderFactory.php create mode 100644 src/CustomForm/Message/CustomFormAnswerNotifyMessage.php create mode 100644 src/CustomForm/Message/Handler/CustomFormAnswerNotifyMessageHandler.php create mode 100644 src/DependencyInjection/Compiler/TreeWalkerDefinitionFactoryCompilerPass.php delete mode 100644 src/Doctrine/EventSubscriber/SettingLifeCycleSubscriber.php create mode 100644 src/Entity/AbstractDateTimedPositioned.php create mode 100644 src/Entity/FieldAwareEntityTrait.php delete mode 100644 src/Entity/LoginAttempt.php create mode 100644 src/Event/Redirection/PostCreatedRedirectionEvent.php create mode 100644 src/Event/Redirection/PostDeletedRedirectionEvent.php create mode 100644 src/Event/Redirection/PostUpdatedRedirectionEvent.php create mode 100644 src/Event/Redirection/RedirectionEvent.php delete mode 100644 src/EventSubscriber/AttributeValueIndexingSubscriber.php create mode 100644 src/EventSubscriber/NodesSourcesAddHeadersSubscriber.php create mode 100644 src/EventSubscriber/RedirectionCacheSubscriber.php delete mode 100644 src/Exception/EmptySaltException.php delete mode 100644 src/Exception/ExceptionViewer.php delete mode 100644 src/Exception/TooManyLoginAttemptsException.php create mode 100644 src/Form/AttributeValueRealmType.php delete mode 100644 src/Form/Constraint/UniqueEntity.php delete mode 100644 src/Form/Constraint/UniqueEntityValidator.php create mode 100644 src/Form/DataListTextType.php create mode 100644 src/ListManager/SessionListFilters.php rename src/{ => Logger}/Entity/Log.php (60%) create mode 100644 src/NodeType/ApiResourceOperationNameGenerator.php delete mode 100644 src/Repository/LoginAttemptRepository.php delete mode 100644 src/Routing/DeferredRouteCollection.php delete mode 100644 src/Routing/InstallRouteCollection.php delete mode 100644 src/Routing/StaticRouter.php create mode 100644 src/SearchEngine/Event/AbstractSearchQueryEvent.php create mode 100644 src/SearchEngine/Event/DocumentSearchQueryEvent.php create mode 100644 src/SearchEngine/Event/NodeSourceSearchQueryEvent.php create mode 100644 src/SearchEngine/SolariumLogger.php create mode 100644 src/SearchEngine/SolrSearchResultItem.php create mode 100644 src/SearchEngine/Subscriber/AttributeValueIndexingSubscriber.php create mode 100644 src/SearchEngine/Subscriber/TreeWalkerIndexingEventSubscriber.php delete mode 100644 src/Security/Authentication/Manager/LoginAttemptManager.php create mode 100644 src/Serializer/ObjectConstructor/AttributeObjectConstructor.php delete mode 100644 src/Serializer/ObjectConstructor/NodeTypeFieldObjectConstructor.php create mode 100644 src/Test/NodeType/ApiResourceGeneratorTest.php create mode 100644 src/TwigExtension/LogExtension.php create mode 100644 templates/DataCollector/solarium.html.twig create mode 100644 templates/DataCollector/solr.svg create mode 100644 tests/bootstrap.php create mode 100644 tests/expected_api_resources/nssecondtest.yml create mode 100644 tests/expected_api_resources/nstest.yml create mode 100644 tests/expected_api_resources/web_response.yml create mode 100644 tests/expected_api_resources/web_response_multiple.yml create mode 100644 tests/object-manager.php create mode 100644 translations/security.ar.xlf create mode 100644 translations/security.de.xlf create mode 100644 translations/security.es.xlf create mode 100644 translations/security.id.xlf create mode 100644 translations/security.it.xlf create mode 100644 translations/security.ru.xlf create mode 100644 translations/security.sr.xlf create mode 100644 translations/security.tr.xlf create mode 100644 translations/security.uk.xlf create mode 100644 translations/security.zh.xlf diff --git a/.env b/.env new file mode 100644 index 00000000..0e52df3d --- /dev/null +++ b/.env @@ -0,0 +1 @@ +# default env file diff --git a/.env.test b/.env.test new file mode 100644 index 00000000..9e7162f0 --- /dev/null +++ b/.env.test @@ -0,0 +1,6 @@ +# define your env variables for the test env here +KERNEL_CLASS='App\Kernel' +APP_SECRET='$ecretf0rt3st' +SYMFONY_DEPRECATIONS_HELPER=999999 +PANTHER_APP_ENV=panther +PANTHER_ERROR_SCREENSHOT_DIR=./var/error-screenshots diff --git a/.github/workflows/run-test.yml b/.github/workflows/run-test.yml index 3b17d56e..189e3a55 100644 --- a/.github/workflows/run-test.yml +++ b/.github/workflows/run-test.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php-version: ['8.0', '8.1'] + php-version: ['8.1', '8.2', '8.3'] steps: - uses: shivammathur/setup-php@v2 with: diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 316f0e9e..00000000 --- a/.travis.yml +++ /dev/null @@ -1,14 +0,0 @@ -language: php -php: - - '8.0' - - '8.1' - - 'nightly' -jobs: - allow_failures: - - php: 'nightly' -install: - - composer install --dev --no-scripts --no-suggest - -script: - - vendor/bin/phpcs -p ./src - - vendor/bin/phpstan analyse -c phpstan.neon diff --git a/LICENSE.md b/LICENSE.md index d4d8a009..8e18fa63 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright © 2023 Ambroise Maupate +Copyright © 2024 Ambroise Maupate Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/composer.json b/composer.json index f98a3974..5dc81bc4 100644 --- a/composer.json +++ b/composer.json @@ -4,6 +4,7 @@ "keywords": [ "cms", "backoffice", + "roadiz", "rezo zero" ], "authors": [ @@ -15,79 +16,79 @@ } ], "type": "symfony-bundle", + "prefer-stable": true, "require": { - "php": ">=8.0", + "php": ">=8.1", "ext-ctype": "*", "ext-iconv": "*", "ext-zip": "*", "ext-json": "*", - "api-platform/core": "~2.7.0", + "api-platform/core": "~3.2.14", "doctrine/annotations": "^1.0", "doctrine/doctrine-bundle": "^2.8.1", "doctrine/doctrine-migrations-bundle": "^3.1", - "doctrine/orm": "<2.17", + "doctrine/orm": "~2.19.0", "gedmo/doctrine-extensions": "^3.10.0", "inlinestyle/inlinestyle": "~1.2.7", "james-heinrich/getid3": "^1.9", "jms/serializer": "^3.9.0", - "jms/serializer-bundle": "^3.10.0", + "jms/serializer-bundle": "^4.2.0", "league/flysystem": "^3.0", "league/flysystem-bundle": "^3.0", - "lexik/jwt-authentication-bundle": "^2.13", + "lexik/jwt-authentication-bundle": "^2.19", "phpdocumentor/reflection-docblock": "^5.2", "phpoffice/phpspreadsheet": "^1.15", "ramsey/uuid": "^4.7", - "rezozero/crypto": "^1.0.0", "rezozero/intervention-request-bundle": "~3.0.0", - "rezozero/liform-bundle": "^0.18.1", + "rezozero/liform-bundle": "^0.19", "rezozero/tree-walker": "^1.3.0", - "roadiz/doc-generator": "2.1.*", - "roadiz/documents": "2.1.*", - "roadiz/dts-generator": "2.1.*", - "roadiz/entity-generator": "2.1.*", - "roadiz/jwt": "2.1.*", - "roadiz/markdown": "2.1.*", - "roadiz/models": "2.1.*", + "roadiz/doc-generator": "2.3.*", + "roadiz/documents": "2.3.*", + "roadiz/dts-generator": "2.3.*", + "roadiz/entity-generator": "2.3.*", + "roadiz/jwt": "2.3.*", + "roadiz/markdown": "2.3.*", + "roadiz/models": "2.3.*", "roadiz/nodetype-contracts": "~1.1.2", - "roadiz/random": "2.1.*", + "roadiz/random": "2.3.*", "rollerworks/password-common-list": "^0.2.0", "rollerworks/password-strength-bundle": "^2.2", "scienta/doctrine-json-functions": "^4.2", "sensio/framework-extra-bundle": "^6.1", "solarium/solarium": "^6.0.4", - "symfony-cmf/routing": "^2.3.3", - "symfony-cmf/routing-bundle": "^2.5", - "symfony/asset": "5.4.*", - "symfony/cache": "5.4.*", - "symfony/console": "5.4.*", - "symfony/dotenv": "5.4.*", - "symfony/expression-language": "5.4.*", - "symfony/flex": "^v1.19.4 || ^2.2.3", - "symfony/form": "5.4.*", - "symfony/framework-bundle": "5.4.*", - "symfony/http-client": "5.4.*", - "symfony/intl": "5.4.*", - "symfony/mailer": "5.4.*", - "symfony/messenger": "5.4.*", - "symfony/mime": "5.4.*", + "symfony-cmf/routing-bundle": "^3.0.2", + "symfony/asset": "6.4.*", + "symfony/cache": "6.4.*", + "symfony/console": "6.4.*", + "symfony/dotenv": "6.4.*", + "symfony/expression-language": "6.4.*", + "symfony/flex": "^2.2.3", + "symfony/form": "6.4.*", + "symfony/framework-bundle": "6.4.*", + "symfony/http-client": "6.4.*", + "symfony/intl": "6.4.*", + "symfony/lock": "6.4.*", + "symfony/mailer": "6.4.*", + "symfony/messenger": "6.4.*", + "symfony/mime": "6.4.*", "symfony/monolog-bundle": "^3.1", - "symfony/notifier": "5.4.*", - "symfony/process": "5.4.*", - "symfony/property-access": "5.4.*", - "symfony/property-info": "5.4.*", - "symfony/proxy-manager-bridge": "5.4.*", - "symfony/rate-limiter": "5.4.*", - "symfony/runtime": "5.4.*", - "symfony/security-core": "5.4.*", - "symfony/serializer": "5.4.*", - "symfony/string": "5.4.*", - "symfony/translation": "5.4.*", - "symfony/twig-bundle": "5.4.*", - "symfony/validator": "5.4.*", - "symfony/web-link": "5.4.*", - "symfony/workflow": "5.4.*", - "symfony/yaml": "5.4.*", - "twig/extra-bundle": "^2.12|^3.0", + "symfony/notifier": "6.4.*", + "symfony/process": "6.4.*", + "symfony/property-access": "6.4.*", + "symfony/property-info": "6.4.*", + "symfony/proxy-manager-bridge": "6.4.*", + "symfony/rate-limiter": "6.4.*", + "symfony/runtime": "6.4.*", + "symfony/security-core": "6.4.*", + "symfony/serializer": "6.4.*", + "symfony/string": "6.4.*", + "symfony/translation": "6.4.*", + "symfony/twig-bundle": "6.4.*", + "symfony/validator": "6.4.*", + "symfony/web-link": "6.4.*", + "symfony/workflow": "6.4.*", + "symfony/yaml": "6.4.*", + "twig/extra-bundle": "^3.0", "twig/intl-extra": "*", "twig/string-extra": "*", "twig/twig": "^3.1" @@ -101,9 +102,9 @@ "phpstan/phpstan-doctrine": "^1.3", "phpunit/phpunit": "^9.5", "squizlabs/php_codesniffer": "^3.5", - "symfony/browser-kit": "5.4.*", - "symfony/phpunit-bridge": "5.4.*", - "symfony/stopwatch": "5.4.*" + "symfony/browser-kit": "6.4.*", + "symfony/phpunit-bridge": "^7.0", + "symfony/stopwatch": "6.4.*" }, "config": { "optimize-autoloader": true, @@ -130,8 +131,8 @@ }, "extra": { "branch-alias": { - "dev-main": "2.1.x-dev", - "dev-develop": "2.2.x-dev" + "dev-main": "2.3.x-dev", + "dev-develop": "2.4.x-dev" } } } diff --git a/config/api_resources/attribute.yml b/config/api_resources/attribute.yml index 11a3c2d6..07ccee13 100644 --- a/config/api_resources/attribute.yml +++ b/config/api_resources/attribute.yml @@ -1,5 +1,4 @@ ---- -RZ\Roadiz\CoreBundle\Entity\Attribute: - collectionOperations: [] - itemOperations: [] - +#resources: +# RZ\Roadiz\CoreBundle\Entity\Attribute: +# operations: [] +# diff --git a/config/api_resources/attribute_value.yml b/config/api_resources/attribute_value.yml index aba402ae..50fd10fe 100644 --- a/config/api_resources/attribute_value.yml +++ b/config/api_resources/attribute_value.yml @@ -1,15 +1,14 @@ ---- -RZ\Roadiz\CoreBundle\Entity\AttributeValue: - collectionOperations: - get: - method: "GET" - normalization_context: - groups: ["urls", "attribute", "document_display", "attribute_node", "attribute_documents"] - enable_max_depth: true - itemOperations: - get: - method: 'GET' - normalization_context: - groups: ["urls", "attribute", "document_display", "attribute_node", "attribute_documents"] - enable_max_depth: true - +#resources: +# RZ\Roadiz\CoreBundle\Entity\AttributeValue: +# operations: +# ApiPlatform\Metadata\GetCollection: +# method: "GET" +# normalizationContext: +# groups: ["urls", "attribute", "document_display", "attribute_node", "attribute_documents"] +# enable_max_depth: true +# ApiPlatform\Metadata\Get: +# method: 'GET' +# normalizationContext: +# groups: ["urls", "attribute", "document_display", "attribute_node", "attribute_documents"] +# enable_max_depth: true +# diff --git a/config/api_resources/common_content.yml b/config/api_resources/common_content.yml new file mode 100644 index 00000000..a02a765a --- /dev/null +++ b/config/api_resources/common_content.yml @@ -0,0 +1,29 @@ +#resources: +# App\Api\Model\CommonContent: +# operations: +# getCommonContent: +# class: ApiPlatform\Metadata\Get +# method: 'GET' +# uriTemplate: '/common_content' +# read: false +# controller: App\Controller\GetCommonContentController +# pagination_enabled: false +# normalizationContext: +# enable_max_depth: true +# pagination_enabled: false +# groups: +# - get +# - common_content +# - web_response +# - walker +# - walker_level +# - children +# - children_count +# - nodes_sources_base +# - nodes_sources_default +# - urls +# - blocks_urls +# - tag_base +# - translation_base +# - document_display +# - document_folders diff --git a/config/api_resources/custom_form.yml b/config/api_resources/custom_form.yml index a1db8c54..0a4f26de 100644 --- a/config/api_resources/custom_form.yml +++ b/config/api_resources/custom_form.yml @@ -1,205 +1,110 @@ ---- -RZ\Roadiz\CoreBundle\Entity\CustomForm: - collectionOperations: - get: - method: "GET" - normalization_context: - enable_max_depth: true - - itemOperations: - get: - method: 'GET' - normalization_context: - enable_max_depth: true - - api_custom_forms_item_post: - method: 'POST' - route_name: api_custom_forms_item_post - normalization_context: - enable_max_depth: true - openapi_context: - summary: Post a user custom form - description: | - Post a user custom form - requestBody: - content: - multipart/form-data: - schema: - type: object - properties: - custom_form_slug[email]: - type: string - example: test@test.test - custom_form_slug[first_name]: - type: string - example: John - custom_form_slug[last_name]: - type: string - example: Doe - responses: - 201: ~ - 400: - description: Posted custom form has errors - content: - application/json: - schema: - type: object - properties: - email: - type: object - example: - email: This value is not a valid email address. - 202: - description: Posted custom form was accepted - content: - application/json: - schema: - type: object - properties: { } - - api_custom_forms_item_definition: - method: 'GET' - route_name: api_custom_forms_item_definition - normalization_context: - enable_max_depth: true - openapi_context: - summary: Get a custom form definition for frontend - description: | - Get a custom form definition for frontend - responses: - 200: - description: Custom form definition object - content: - application/json: - schema: - type: object - properties: - title: - type: string - description: Form inputs prefix - example: reiciendis_natus_ducimus_nostrum - type: - type: string - description: Form definition type - example: object - properties: - type: object - description: Form definition fields - example: - email: - type: string - title: Email - attr: - data-group: null - placeholder: null - widget: email - propertyOrder: 1 - first_name: - type: string - title: Firstname - attr: - data-group: null - placeholder: null - widget: string - propertyOrder: 2 - required: - type: array - description: Required fields names - example: - - 'email' - - api_contact_form_post: - method: 'POST' - route_name: api_contact_form_post - normalization_context: - enable_max_depth: true - openapi_context: - summary: Post a user contact form - description: | - Post a user contact form - requestBody: - content: - multipart/form-data: - schema: - type: object - properties: - email: - type: string - example: test@test.test - first_name: - type: string - example: John - last_name: - type: string - example: Doe - responses: - 201: ~ - 400: - description: Posted contact form has errors - content: - application/json: - schema: - type: object - properties: - email: - type: object - example: - email: This value is not a valid email address. - 202: - description: Posted contact form was accepted - content: - application/json: - schema: - type: object - properties: { } - - api_contact_form_definition: - method: 'GET' - route_name: api_contact_form_definition - normalization_context: - enable_max_depth: true - openapi_context: - summary: Get a contact form definition for frontend - description: | - Get a contact form definition for frontend - responses: - 200: - description: Contact form definition object - content: - application/json: - schema: - type: object - properties: - title: - type: string - description: Form inputs prefix - example: reiciendis_natus_ducimus_nostrum - type: - type: string - description: Form definition type - example: object - properties: - type: object - description: Form definition fields - example: - email: - type: string - title: Email - attr: - data-group: null - placeholder: null - widget: email - propertyOrder: 1 - first_name: - type: string - title: Firstname - attr: - data-group: null - placeholder: null - widget: string - propertyOrder: 2 - required: - type: array - description: Required fields names - example: - - 'email' +#resources: +# RZ\Roadiz\CoreBundle\Entity\CustomForm: +# operations: +# ApiPlatform\Metadata\GetCollection: +# method: "GET" +# normalizationContext: +# enable_max_depth: true +# +# ApiPlatform\Metadata\Get: +# method: 'GET' +# normalizationContext: +# enable_max_depth: true +# +# api_custom_forms_item_post: +# method: 'POST' +# class: ApiPlatform\Metadata\Post +# routeName: api_custom_forms_item_post +# normalizationContext: +# enable_max_depth: true +# openapiContext: +# summary: Post a user custom form +# description: | +# Post a user custom form +# requestBody: +# content: +# multipart/form-data: +# schema: +# type: object +# properties: +# custom_form_slug[email]: +# type: string +# example: test@test.test +# custom_form_slug[first_name]: +# type: string +# example: John +# custom_form_slug[last_name]: +# type: string +# example: Doe +# responses: +# 201: ~ +# 400: +# description: Posted custom form has errors +# content: +# application/json: +# schema: +# type: object +# properties: +# email: +# type: object +# example: +# email: This value is not a valid email address. +# 202: +# description: Posted custom form was accepted +# content: +# application/json: +# schema: +# type: object +# properties: { } +# +# api_custom_forms_item_definition: +# method: 'GET' +# class: ApiPlatform\Metadata\Get +# routeName: api_custom_forms_item_definition +# normalizationContext: +# enable_max_depth: true +# openapiContext: +# summary: Get a custom form definition for frontend +# description: | +# Get a custom form definition for frontend +# responses: +# 200: +# description: Custom form definition object +# content: +# application/json: +# schema: +# type: object +# properties: +# title: +# type: string +# description: Form inputs prefix +# example: reiciendis_natus_ducimus_nostrum +# type: +# type: string +# description: Form definition type +# example: object +# properties: +# type: object +# description: Form definition fields +# example: +# email: +# type: string +# title: Email +# attr: +# data-group: null +# placeholder: null +# widget: email +# propertyOrder: 1 +# first_name: +# type: string +# title: Firstname +# attr: +# data-group: null +# placeholder: null +# widget: string +# propertyOrder: 2 +# required: +# type: array +# description: Required fields names +# example: +# - 'email' diff --git a/config/api_resources/document.yml b/config/api_resources/document.yml index 9f3f075c..db91f92b 100644 --- a/config/api_resources/document.yml +++ b/config/api_resources/document.yml @@ -1,15 +1,15 @@ ---- -RZ\Roadiz\CoreBundle\Entity\Document: - collectionOperations: - get: - method: "GET" - normalization_context: - groups: ["urls", "document_display", "document_display_sources", "position"] - enable_max_depth: true - itemOperations: - get: - method: 'GET' - normalization_context: - groups: ["urls", "document", "document_display", "document_folders", "document_display_sources", "position"] - enable_max_depth: true - +#resources: +# RZ\Roadiz\CoreBundle\Entity\Document: +# operations: +# ApiPlatform\Metadata\GetCollection: +# method: "GET" +# normalizationContext: +# groups: ["urls", "document_display", "document_folders", "document_folders_all", "document_display_sources"] +# enable_max_depth: true +# +# ApiPlatform\Metadata\Get: +# method: 'GET' +# normalizationContext: +# groups: ["urls", "document", "document_display", "document_folders", "document_folders_all", "document_display_sources"] +# enable_max_depth: true +# diff --git a/config/api_resources/folder.yml b/config/api_resources/folder.yml index c29268fd..01016890 100644 --- a/config/api_resources/folder.yml +++ b/config/api_resources/folder.yml @@ -1,11 +1,13 @@ ---- -RZ\Roadiz\CoreBundle\Entity\Folder: - iri: Folder - shortName: Folder - collectionOperations: {} - itemOperations: - get: - method: "GET" - normalization_context: - groups: [ "folder", "position" ] - enable_max_depth: true +#resources: +# RZ\Roadiz\CoreBundle\Entity\Folder: +# operations: +# ApiPlatform\Metadata\GetCollection: +# method: "GET" +# normalizationContext: +# groups: [ "folder" ] +# enable_max_depth: true +# ApiPlatform\Metadata\Get: +# method: "GET" +# normalizationContext: +# groups: [ "folder" ] +# enable_max_depth: true diff --git a/config/api_resources/node.yml b/config/api_resources/node.yml index 4a3deb0b..ce20501e 100644 --- a/config/api_resources/node.yml +++ b/config/api_resources/node.yml @@ -1,10 +1,8 @@ - -RZ\Roadiz\CoreBundle\Entity\Node: - shortName: Node - collectionOperations: {} - itemOperations: - get: - method: 'GET' - normalization_context: - enable_max_depth: true - groups: ["node", "tag_base", "translation_base", "document_display"] +#resources: +# RZ\Roadiz\CoreBundle\Entity\Node: +# operations: +# ApiPlatform\Metadata\Get: +# method: 'GET' +# normalizationContext: +# groups: ["node", "document_display"] +# enable_max_depth: true diff --git a/config/api_resources/nodes_sources.yml b/config/api_resources/nodes_sources.yml index d6f10d0f..9b379c70 100644 --- a/config/api_resources/nodes_sources.yml +++ b/config/api_resources/nodes_sources.yml @@ -1,55 +1,66 @@ ---- -RZ\Roadiz\CoreBundle\Entity\NodesSources: - iri: NodesSources - shortName: NodesSources - collectionOperations: - # Get operation is needed for sitemap generation - get: - method: "GET" - normalization_context: - enable_max_depth: true - groups: - - nodes_sources_base - - nodes_sources_default - - urls - - tag_base - - translation_base - - document_display - - position -# search: -# method: 'GET' -# path: '/nodes_sources/search' -# controller: App\Controller\SearchNodesSourcesController -# normalization_context: -# groups: -# - nodes_sources_base -# - nodes_sources_default -# - urls -# - tag_base -# - translation_base -# - document_display -# - position -# openapi_context: -# summary: Search NodesSources resources -# description: | -# Search NodesSources resources using **Solr** full-text search engine -# parameters: -# - type: string -# name: search -# in: query -# required: true -# description: Search pattern -# schema: -# type: string - - itemOperations: - get: - method: 'GET' - normalization_context: - enable_max_depth: true - groups: - - nodes_sources - - urls - - tag_base - - translation_base - - document_display +#resources: +# RZ\Roadiz\CoreBundle\Entity\NodesSources: +# operations: +# ApiPlatform\Metadata\GetCollection: +# method: "GET" +# normalizationContext: +# groups: +# - nodes_sources_base +# - nodes_sources_default +# - user +# - urls +# - tag_base +# - translation_base +# - document_display +# +# api_nodes_sources_archives: +# class: ApiPlatform\Metadata\GetCollection +# method: 'GET' +# uriTemplate: '/nodes_sources/archives' +# pagination_enabled: false +# pagination_client_enabled: false +# extraProperties: +# archive_enabled: true +# archive_publication_field_name: publishedAt +# normalizationContext: +# groups: +# - get +# - archives +# openapiContext: +# summary: Get available NodesSources archives +# parameters: ~ +# description: | +# Get available NodesSources archives (years and months) based on their `publishedAt` field +# +# api_nodes_sources_search: +# class: ApiPlatform\Metadata\GetCollection +# method: 'GET' +# uriTemplate: '/nodes_sources/search' +# controller: RZ\Roadiz\CoreBundle\Api\Controller\NodesSourcesSearchController +# read: false +# normalizationContext: +# groups: +# - get +# - nodes_sources_base +# - nodes_sources_default +# - urls +# - tag_base +# - translation_base +# - document_display +# openapiContext: +# summary: Search NodesSources resources +# description: | +# Search all website NodesSources resources using **Solr** full-text search engine +# parameters: +# - type: string +# name: search +# in: query +# required: true +# description: Search pattern +# schema: +# type: string +# +# ApiPlatform\Metadata\Get: +# method: 'GET' +# normalizationContext: +# groups: ["nodes_sources", "urls", "tag_base", "translation_base", "document_display"] diff --git a/config/api_resources/realm.yml b/config/api_resources/realm.yml index 45da623a..f0bc2faa 100644 --- a/config/api_resources/realm.yml +++ b/config/api_resources/realm.yml @@ -1,11 +1,14 @@ ---- -RZ\Roadiz\CoreBundle\Entity\Realm: - iri: Realm - shortName: Realm - collectionOperations: {} - itemOperations: - get: - method: "GET" - normalization_context: - groups: [ "get", "realm" ] - enable_max_depth: true +#resources: +# RZ\Roadiz\CoreBundle\Entity\Realm: +# operations: +# ApiPlatform\Metadata\GetCollection: +# method: "GET" +# normalizationContext: +# groups: [ "realm" ] +# enable_max_depth: true +# +# ApiPlatform\Metadata\Get: +# method: "GET" +# normalizationContext: +# groups: [ "realm" ] +# enable_max_depth: true diff --git a/config/api_resources/tag.yml b/config/api_resources/tag.yml index dc516bb0..e9b64d16 100644 --- a/config/api_resources/tag.yml +++ b/config/api_resources/tag.yml @@ -1,20 +1,18 @@ - -RZ\Roadiz\CoreBundle\Entity\Tag: - iri: Tag - shortName: Tag - collectionOperations: - get: - method: "GET" - normalization_context: - enable_max_depth: true - groups: - - tag - - tag_base - itemOperations: - get: - method: 'GET' - normalization_context: - enable_max_depth: true - groups: - - tag - - tag_base +#resources: +# RZ\Roadiz\CoreBundle\Entity\Tag: +# operations: +# ApiPlatform\Metadata\GetCollection: +# method: "GET" +# normalizationContext: +# enable_max_depth: true +# groups: +# - tag_base +# +# ApiPlatform\Metadata\Get: +# method: 'GET' +# normalizationContext: +# enable_max_depth: true +# groups: +# - tag +# - tag_base +# - tag_parent diff --git a/config/api_resources/translation.yml b/config/api_resources/translation.yml index 304ffa9e..c0d56c1a 100644 --- a/config/api_resources/translation.yml +++ b/config/api_resources/translation.yml @@ -1,9 +1,17 @@ - -RZ\Roadiz\CoreBundle\Entity\Translation: - collectionOperations: - get: - method: "GET" - itemOperations: - get: - method: 'GET' - +#resources: +# RZ\Roadiz\CoreBundle\Entity\Translation: +# operations: +# ApiPlatform\Metadata\GetCollection: +# method: "GET" +# normalizationContext: +# enable_max_depth: true +# groups: +# - translation_base +# +# ApiPlatform\Metadata\Get: +# method: 'GET' +# normalizationContext: +# enable_max_depth: true +# groups: +# - translation_base +# diff --git a/config/api_resources/web_response.yml b/config/api_resources/web_response.yml index 2353a4c1..1c2a1d41 100644 --- a/config/api_resources/web_response.yml +++ b/config/api_resources/web_response.yml @@ -1,39 +1,3 @@ -RZ\Roadiz\CoreBundle\Api\Model\WebResponse: - collectionOperations: {} - itemOperations: - getByPath: - method: 'GET' - path: '/web_response_by_path' - read: false - controller: RZ\Roadiz\CoreBundle\Api\Controller\GetWebResponseByPathController - pagination_enabled: false - normalization_context: - enable_max_depth: true - pagination_enabled: false - groups: - - get - - web_response - - position - - walker - - walker_level - - meta - - children - - children_count - - nodes_sources - - urls - - tag_base - - translation_base - - document_display - - node_attributes - openapi_context: - summary: Get a resource by its path wrapped in a WebResponse object - description: | - Get a resource by its path wrapped in a WebResponse - parameters: - - type: string - name: path - in: query - required: true - description: Resource path, or `/` for home page - schema: - type: string +#resources: +# RZ\Roadiz\CoreBundle\Api\Model\WebResponse: +# operations: [] diff --git a/config/fixtures/settings.json b/config/fixtures/settings.json index 137890ad..ee93f247 100644 --- a/config/fixtures/settings.json +++ b/config/fixtures/settings.json @@ -2,7 +2,6 @@ { "name": "force_locale", "visible": true, - "encrypted": false, "description": "Force displaying translation locale in every node’ paths. This should be *ON* if you redirect users based on their language on homepage.", "setting_group": { "name": "Development", @@ -15,7 +14,6 @@ "name": "force_locale_with_urlaliases", "description": "force_locale_with_urlaliases.help", "visible": true, - "encrypted": false, "setting_group": { "name": "Development", "in_menu": true @@ -25,7 +23,6 @@ { "name": "leaflet_map_tile_url", "value": "https:\/\/{s}.tile.osm.org\/{z}\/{x}\/{y}.png", - "encrypted": false, "description": "Default maps tiles layout when using *Leaflet*.", "visible": true, "setting_group": { @@ -38,7 +35,6 @@ { "name": "maps_default_location", "value": "{\"lat\":45.769785, \"lng\":4.833967, \"zoom\":14}", - "encrypted": false, "description": "Default maps marker location.", "visible": true, "setting_group": { @@ -52,7 +48,6 @@ "name": "openid_button_label", "description": "openid_button_label.help", "visible": true, - "encrypted": false, "setting_group": { "name": "OpenId", "in_menu": true @@ -62,7 +57,6 @@ { "name": "support_email_address", "visible": true, - "encrypted": false, "description": "Support email address, used in every system emails footer", "setting_group": { "name": "Emailings", @@ -74,7 +68,6 @@ { "name": "email_sender", "visible": true, - "encrypted": false, "description": "Default sender email, used as origin for every system email sent. This email **must be allowed by your SMTP server.**", "setting_group": { "name": "Emailings", @@ -86,7 +79,6 @@ { "name": "email_sender_name", "visible": true, - "encrypted": false, "setting_group": { "name": "Emailings", "in_menu": true @@ -97,7 +89,6 @@ { "name": "universal_analytics_id", "visible": true, - "encrypted": false, "setting_group": { "name": "APIs", "in_menu": true @@ -108,18 +99,6 @@ { "name": "google_tag_manager_id", "visible": true, - "encrypted": false, - "setting_group": { - "name": "APIs", - "in_menu": true - }, - "type": 0, - "default_values": "" - }, - { - "name": "instagram_access_token", - "visible": true, - "encrypted": true, "setting_group": { "name": "APIs", "in_menu": true @@ -130,7 +109,6 @@ { "name": "seo_description", "visible": true, - "encrypted": false, "setting_group": { "name": "Site information", "in_menu": true @@ -141,7 +119,6 @@ { "name": "site_name", "visible": true, - "encrypted": false, "setting_group": { "name": "Site information", "in_menu": true @@ -152,7 +129,6 @@ { "name": "maintenance_mode", "visible": true, - "encrypted": false, "description": "Switch maintenance mode. Only login page will be available for public requests.", "setting_group": { "name": "Site information", @@ -164,7 +140,6 @@ { "name": "site_copyright", "visible": true, - "encrypted": false, "setting_group": { "name": "Site information", "in_menu": true @@ -174,7 +149,6 @@ }, { "name": "main_color", - "encrypted": false, "visible": true, "setting_group": { "name": "Site information", @@ -186,7 +160,6 @@ { "name": "admin_image", "visible": true, - "encrypted": false, "setting_group": { "name": "Site information", "in_menu": true @@ -197,7 +170,6 @@ { "name": "login_image", "visible": true, - "encrypted": false, "description": "Replace random *Splashbase* login images with your own.", "setting_group": { "name": "Site information", @@ -209,7 +181,6 @@ { "name": "facebook_url", "visible": true, - "encrypted": false, "setting_group": { "name": "Social networks", "in_menu": true @@ -220,7 +191,6 @@ { "name": "instagram_url", "visible": true, - "encrypted": false, "setting_group": { "name": "Social networks", "in_menu": true @@ -231,7 +201,6 @@ { "name": "pinterest_url", "visible": true, - "encrypted": false, "setting_group": { "name": "Social networks", "in_menu": true @@ -242,7 +211,6 @@ { "name": "twitter_url", "visible": true, - "encrypted": false, "setting_group": { "name": "Social networks", "in_menu": true @@ -253,7 +221,6 @@ { "name": "linkedin_url", "visible": true, - "encrypted": false, "setting_group": { "name": "Social networks", "in_menu": true @@ -264,7 +231,6 @@ { "name": "youtube_url", "visible": true, - "encrypted": false, "setting_group": { "name": "Social networks", "in_menu": true @@ -276,7 +242,6 @@ "name": "custom_preview_scheme", "description": "custom_preview_scheme.help", "visible": true, - "encrypted": false, "setting_group": { "name": "Site information", "in_menu": true @@ -287,7 +252,6 @@ "name": "custom_public_scheme", "description": "custom_public_scheme.help", "visible": true, - "encrypted": false, "setting_group": { "name": "Site information", "in_menu": true @@ -298,7 +262,6 @@ "name": "dashboard_iframe", "description": "dashboard_iframe.help", "visible": true, - "encrypted": false, "setting_group": { "name": "Site information", "in_menu": true diff --git a/config/packages/api_platform.yaml b/config/packages/api_platform.yaml index 0fc9e246..f9551bd7 100644 --- a/config/packages/api_platform.yaml +++ b/config/packages/api_platform.yaml @@ -9,8 +9,8 @@ api_platform: show_webby: false swagger: versions: [3] - title: "My Roadiz website API" - description: "My Roadiz website API" + title: "%env(string:APP_TITLE)%" + description: "%env(string:APP_DESCRIPTION)%" version: '%env(string:APP_VERSION)%' mapping: paths: @@ -19,32 +19,27 @@ api_platform: - '%kernel.project_dir%/vendor/roadiz/core-bundle/src/Entity' - '%kernel.project_dir%/vendor/rezozero/tree-walker/src' - '%kernel.project_dir%/config/api_resources' -# http_cache: -# # Automatically generate etags for API responses. -# etag: true -# public: true -# # Default value for the response max age. -# max_age: '%env(int:HTTP_CACHE_MAX_AGE)%' -# # Default value for the response shared (proxy) max age. -# shared_max_age: '%env(int:HTTP_CACHE_SHARED_MAX_AGE)%' -# # Default values of the "Vary" HTTP header. -# vary: ['Accept', 'Authorization', 'Origin', 'Accept-Encoding', 'Content-Type'] -# invalidation: -# enabled: true -# varnish_urls: ['%env(VARNISH_URL)%'] + + collection: + pagination: + page_parameter_name: page + items_per_page_parameter_name: itemsPerPage + + http_cache: + invalidation: + enabled: true + varnish_urls: [ '%env(VARNISH_URL)%' ] + defaults: + enable_max_depth: true + normalization_context: + skip_null_values: true pagination_client_items_per_page: true pagination_items_per_page: 15 pagination_maximum_items_per_page: 50 -# cache_headers: -# etag: true -# public: true -# max_age: '%env(int:HTTP_CACHE_MAX_AGE)%' -# shared_max_age: '%env(int:HTTP_CACHE_SHARED_MAX_AGE)%' -# vary: ['Accept', 'Authorization', 'Origin', 'Accept-Encoding', 'Content-Type'] - collection: - pagination: - items_per_page: 15 # Default value - maximum_items_per_page: 50 - client_items_per_page: true - items_per_page_parameter_name: itemsPerPage + cache_headers: + etag: true + public: true + max_age: '%env(int:HTTP_CACHE_MAX_AGE)%' + shared_max_age: '%env(int:HTTP_CACHE_SHARED_MAX_AGE)%' + vary: [ 'Accept', 'Authorization', 'Origin', 'Accept-Encoding', 'Content-Type' ] diff --git a/config/packages/doctrine.yaml b/config/packages/doctrine.yaml index 092fadc1..8165a234 100644 --- a/config/packages/doctrine.yaml +++ b/config/packages/doctrine.yaml @@ -5,43 +5,67 @@ doctrine: # either here or in the DATABASE_URL env var (see .env file) #server_version: '13' orm: - dql: - string_functions: - JSON_CONTAINS: Scienta\DoctrineJsonFunctions\Query\AST\Functions\Mysql\JsonContains auto_generate_proxy_classes: true - naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware - auto_mapping: true - mappings: - App: - is_bundle: false - type: attribute - dir: '%kernel.project_dir%/src/Entity' - prefix: 'App\Entity' - alias: App - RoadizCoreBundle: - is_bundle: true - type: attribute - dir: 'src/Entity' - prefix: 'RZ\Roadiz\CoreBundle\Entity' - alias: RoadizCoreBundle - RZ\Roadiz\Core: - is_bundle: false - type: annotation - dir: '%kernel.project_dir%/vendor/roadiz/models/src/Roadiz/Core/AbstractEntities' - prefix: 'RZ\Roadiz\Core\AbstractEntities' - alias: AbstractEntities - App\GeneratedEntity: - is_bundle: false - type: attribute - dir: '%kernel.project_dir%/src/GeneratedEntity' - prefix: 'App\GeneratedEntity' - alias: App\GeneratedEntity - gedmo_loggable: - type: attribute - prefix: Gedmo\Loggable\Entity\MappedSuperclass - dir: "%kernel.project_dir%/vendor/gedmo/doctrine-extensions/src/Loggable/Entity/MappedSuperclass" - alias: GedmoLoggableMappedSuperclass - is_bundle: false + default_entity_manager: default + entity_managers: + # Put `logger` entity manager first to select it as default for Log entity + logger: + naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware + mappings: + ## Just sharding EM to avoid having Logs in default EM + ## and flushing bad entities when storing log entries. + RoadizCoreLogger: + is_bundle: false + type: attribute + dir: '%kernel.project_dir%/vendor/roadiz/core-bundle/src/Logger/Entity' + prefix: 'RZ\Roadiz\CoreBundle\Logger\Entity' + alias: RoadizCoreLogger + default: + dql: + string_functions: + JSON_CONTAINS: Scienta\DoctrineJsonFunctions\Query\AST\Functions\Mysql\JsonContains + naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware + auto_mapping: true + mappings: + ## Keep RoadizCoreLogger to avoid creating different migrations since we are using + ## the same database for both entity managers. Just sharding EM to avoid + ## having Logs in default EM and flushing bad entities when storing log entries. + RoadizCoreLogger: + is_bundle: false + type: attribute + dir: '%kernel.project_dir%/vendor/roadiz/core-bundle/src/Logger/Entity' + prefix: 'RZ\Roadiz\CoreBundle\Logger\Entity' + alias: RoadizCoreLogger + App: + is_bundle: false + type: attribute + dir: '%kernel.project_dir%/src/Entity' + prefix: 'App\Entity' + alias: App + RoadizCoreBundle: + is_bundle: true + type: attribute + dir: 'src/Entity' + prefix: 'RZ\Roadiz\CoreBundle\Entity' + alias: RoadizCoreBundle + RZ\Roadiz\Core: + is_bundle: false + type: annotation + dir: '%kernel.project_dir%/vendor/roadiz/models/src/Core/AbstractEntities' + prefix: 'RZ\Roadiz\Core\AbstractEntities' + alias: AbstractEntities + App\GeneratedEntity: + is_bundle: false + type: attribute + dir: '%kernel.project_dir%/src/GeneratedEntity' + prefix: 'App\GeneratedEntity' + alias: App\GeneratedEntity + gedmo_loggable: + type: attribute + prefix: Gedmo\Loggable\Entity\MappedSuperclass + dir: "%kernel.project_dir%/vendor/gedmo/doctrine-extensions/src/Loggable/Entity/MappedSuperclass" + alias: GedmoLoggableMappedSuperclass + is_bundle: false resolve_target_entities: Symfony\Component\Security\Core\User\UserInterface: RZ\Roadiz\CoreBundle\Entity\User diff --git a/config/packages/roadiz_core.yaml b/config/packages/roadiz_core.yaml index 818584c3..1f5bf32d 100644 --- a/config/packages/roadiz_core.yaml +++ b/config/packages/roadiz_core.yaml @@ -11,9 +11,6 @@ roadiz_core: # Be careful if you are using a reverse-proxy cache, YOU MUST vary on Accept-Language header and normalize it. # @see https://varnish-cache.org/docs/6.3/users-guide/increasing-your-hitrate.html#http-vary useAcceptLanguageHeader: '%env(bool:APP_USE_ACCEPT_LANGUAGE_HEADER)%' - security: - private_key_name: default - themes: [] medias: unsplash_client_id: '%env(string:APP_UNSPLASH_CLIENT_ID)%' soundcloud_client_id: '%env(string:APP_SOUNDCLOUD_CLIENT_ID)%' diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 4c2ce00a..8324dc43 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -8,6 +8,8 @@ security: # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers providers: + jwt: + lexik_jwt: ~ roadiz_user_provider: entity: class: RZ\Roadiz\CoreBundle\Entity\User @@ -20,12 +22,12 @@ security: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ security: false - # JWT for API - api: - pattern: ^/api + + # https://symfony.com/bundles/LexikJWTAuthenticationBundle/current/index.html#configure-application-routing + api_login: stateless: true + pattern: ^/api/token provider: all_users - user_checker: RZ\Roadiz\CoreBundle\Security\UserChecker login_throttling: max_attempts: 3 json_login: @@ -34,7 +36,21 @@ security: password_path: password success_handler: lexik_jwt_authentication.handler.authentication_success failure_handler: lexik_jwt_authentication.handler.authentication_failure + user_checker: RZ\Roadiz\CoreBundle\Security\UserChecker + + # https://symfony.com/bundles/LexikJWTAuthenticationBundle/current/8-jwt-user-provider.html#symfony-5-3-and-higher + api: + pattern: ^/api + stateless: true + # Do not reload user from database, trust JWT roles in order to restrict PreviewUsers + # Only drawback is when you want to disable / block / expire a user, you'll have to + # wait for JWT token to expire. + provider: jwt + # If you really want to reload user from database, uncomment this line, but Preview JWT + # will be reloaded as full user and not as PreviewUser. + #provider: all_users jwt: ~ + # disables session creation for assets and healthcheck controllers assets: pattern: ^/assets @@ -63,18 +79,16 @@ security: max_attempts: 3 logout: path: logoutPage - guard: - authenticators: - - lexik_jwt_authentication.jwt_token_authenticator custom_authenticator: - RZ\Roadiz\RozierBundle\Security\RozierAuthenticator # Easy way to control access for large sections of your site # Note: Only the *first* access control that matches will be used access_control: - - { path: ^/rz-admin/login, roles: IS_AUTHENTICATED_ANONYMOUSLY } + - { path: ^/rz-admin/login, roles: PUBLIC_ACCESS } + - { path: ^/rz-admin/logout, roles: PUBLIC_ACCESS } - { path: ^/rz-admin, roles: ROLE_BACKEND_USER } - - { path: ^/api/token, roles: IS_AUTHENTICATED_ANONYMOUSLY } - - { path: "^/api/custom_forms/(?:[0-9]+)/post", methods: [ POST ], roles: IS_AUTHENTICATED_ANONYMOUSLY } + - { path: ^/api/token, roles: PUBLIC_ACCESS } + - { path: "^/api/custom_forms/(?:[0-9]+)/post", methods: [ POST ], roles: PUBLIC_ACCESS } - { path: ^/api, roles: ROLE_BACKEND_USER, methods: [ POST, PUT, PATCH, DELETE ] } # - { path: ^/profile, roles: ROLE_USER } diff --git a/config/routing.yaml b/config/routing.yaml index 361fab01..e3a597bc 100644 --- a/config/routing.yaml +++ b/config/routing.yaml @@ -5,12 +5,14 @@ api_custom_forms_item_definition: methods: [GET] path: /api/custom_forms/{id}/definition controller: RZ\Roadiz\CoreBundle\Controller\CustomFormController::definitionAction + stateless: true requirements: id: "[0-9]+" api_custom_forms_item_post: methods: [POST] path: /api/custom_forms/{id}/post controller: RZ\Roadiz\CoreBundle\Controller\CustomFormController::postAction + stateless: true requirements: id: "[0-9]+" @@ -28,12 +30,10 @@ customFormSentAction: healthCheckAction: methods: [GET] path: /health-check + stateless: true controller: RZ\Roadiz\CoreBundle\Controller\HealthCheckController -roadiz_core_themes: - resource: . - type: themes - api_login_check: methods: [POST] + stateless: true path: /api/token diff --git a/config/services.yaml b/config/services.yaml index 7c490b88..eb48f24e 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -1,6 +1,6 @@ --- parameters: - roadiz_core.cms_version: '2.1.63' + roadiz_core.cms_version: '2.3.10' roadiz_core.cms_version_prefix: 'main' env(APP_NAMESPACE): "roadiz" env(APP_VERSION): "0.1.0" @@ -27,21 +27,21 @@ services: $cmsVersion: '%roadiz_core.cms_version%' $appVersion: '%roadiz_core.app_version%' $cmsVersionPrefix: '%roadiz_core.cms_version_prefix%' - $staticDomain: '%roadiz_core.static_domain_name%' $hideRoadizVersion: '%roadiz_core.hide_roadiz_version%' $inheritanceType: '%roadiz_core.inheritance_type%' $maxPixelSize: '%rz_intervention_request.max_pixel_size%' $appNamespace: '%roadiz_core.app_namespace%' $projectDir: '%kernel.project_dir%' $exportDir: '%kernel.project_dir%/var/export' - $privateKeyName: '%roadiz_core.private_key_name%' $generatedEntitiesDir: '%roadiz_core.generated_entities_dir%' $serializedNodeTypesDir: '%roadiz_core.serialized_node_types_dir%' $importFilesConfigPath: '%roadiz_core.import_files_config_path%' $kernelProjectDir: '%kernel.project_dir%' $apiResourcesDir: '%kernel.project_dir%/config/api_resources' $debug: '%kernel.debug%' + $kernelEnvironment: '%kernel.environment%' $defaultControllerClass: '%roadiz_core.default_node_source_controller%' + $defaultLocale: '%kernel.default_locale%' $webhookMessageTypes: '%roadiz_core.webhook.message_types%' $useAcceptLanguageHeader: '%roadiz_core.use_accept_language_header%' $healthCheckToken: '%roadiz_core.health_check_token%' @@ -50,6 +50,8 @@ services: $maxVersionsShowed: '%roadiz_core.max_versions_showed%' $recaptchaPublicKey: '%roadiz_core.medias.recaptcha_public_key%' $recaptchaPrivateKey: '%roadiz_core.medias.recaptcha_private_key%' + $webResponseClass: '%roadiz_core.web_response_class%' + $useGravatar: '%roadiz_core.use_gravatar%' RZ\Roadiz\CoreBundle\: resource: '../src/' @@ -113,6 +115,21 @@ services: # Extension must be called after all filtering BUT before default pagination extension tags: [ { name: 'api_platform.doctrine.orm.query_extension.collection', priority: -40 } ] + # + # These API doctrine extension must be called last before pagination + # to perform on existing JOIN with node entities (found after filtering) + # + RZ\Roadiz\CoreBundle\Api\Extension\AttributeValueQueryExtension: + tags: [ + { name: 'api_platform.doctrine.orm.query_extension.collection', priority: -40 }, + { name: 'api_platform.doctrine.orm.query_extension.item', priority: -40 }, + ] + RZ\Roadiz\CoreBundle\Api\Extension\NodesTagsQueryExtension: + tags: [ + { name: 'api_platform.doctrine.orm.query_extension.collection', priority: -40 }, + { name: 'api_platform.doctrine.orm.query_extension.item', priority: -40 }, + ] + RZ\Roadiz\CoreBundle\Bag\: resource: '../src/Bag/' autowire: true @@ -370,11 +387,16 @@ services: RZ\Roadiz\CoreBundle\Preview\RequestPreviewRevolver: arguments: - '@Symfony\Component\HttpFoundation\RequestStack' - - 'ROLE_BACKEND_USER' + - '%roadiz_core.preview_required_role_name%' RZ\Roadiz\CoreBundle\SearchEngine\ClientRegistry: arguments: ['@service_container'] + RZ\Roadiz\CoreBundle\SearchEngine\SolariumLogger: + tags: + - { name: data_collector, template: '@RoadizCore/DataCollector/solarium.html.twig', id: 'solarium' } + - { name: monolog.logger, channel: solr } + RZ\Roadiz\CoreBundle\SearchEngine\Indexer\IndexerFactory: arguments: ['@service_container'] @@ -415,12 +437,6 @@ services: RZ\Roadiz\Random\PasswordGenerator: ~ - RZ\Crypto\KeyChain\KeyChainInterface: - alias: RZ\Crypto\KeyChain\AsymmetricFilesystemKeyChain - - RZ\Crypto\KeyChain\AsymmetricFilesystemKeyChain: - arguments: ['%kernel.project_dir%/var/secret', true] - JMS\Serializer\Construction\ObjectConstructorInterface: alias: RZ\Roadiz\CoreBundle\Serializer\ObjectConstructor\ObjectConstructor @@ -431,10 +447,10 @@ services: - '@RZ\Roadiz\CoreBundle\Serializer\ObjectConstructor\ObjectConstructor' - [ '@RZ\Roadiz\CoreBundle\Serializer\ObjectConstructor\TranslationObjectConstructor', + '@RZ\Roadiz\CoreBundle\Serializer\ObjectConstructor\AttributeObjectConstructor', '@RZ\Roadiz\CoreBundle\Serializer\ObjectConstructor\TagObjectConstructor', '@RZ\Roadiz\CoreBundle\Serializer\ObjectConstructor\NodeObjectConstructor', '@RZ\Roadiz\CoreBundle\Serializer\ObjectConstructor\NodeTypeObjectConstructor', - '@RZ\Roadiz\CoreBundle\Serializer\ObjectConstructor\NodeTypeFieldObjectConstructor', '@RZ\Roadiz\CoreBundle\Serializer\ObjectConstructor\RoleObjectConstructor', '@RZ\Roadiz\CoreBundle\Serializer\ObjectConstructor\GroupObjectConstructor', '@RZ\Roadiz\CoreBundle\Serializer\ObjectConstructor\SettingObjectConstructor', @@ -444,7 +460,6 @@ services: Solarium\Core\Client\Client: factory: ['RZ\Roadiz\CoreBundle\SearchEngine\ClientRegistry', 'getClient'] - # Provides LoginAttemptManager aware authentication RZ\Roadiz\CoreBundle\Security\Authentication\JwtAuthenticationSuccessHandler: decorates: 'lexik_jwt_authentication.handler.authentication_success' @@ -513,9 +528,9 @@ services: RZ\Roadiz\Documents\Renderer\PdfRenderer: tags: [ 'roadiz_core.document_renderer' ] RZ\Roadiz\Documents\Renderer\SvgRenderer: - tags: [ 'roadiz_core.document_renderer' ] + tags: [ { name: 'roadiz_core.document_renderer', priority: 10 } ] RZ\Roadiz\Documents\Renderer\InlineSvgRenderer: - tags: [ 'roadiz_core.document_renderer' ] + tags: [ { name: 'roadiz_core.document_renderer', priority: 11 } ] RZ\Roadiz\Documents\Renderer\EmbedRenderer: tags: [ { name: 'roadiz_core.document_renderer', priority: -128 } ] RZ\Roadiz\Documents\Renderer\ThumbnailRenderer: @@ -531,7 +546,6 @@ services: alias: RZ\Roadiz\CoreBundle\Filesystem\RoadizFileDirectories public: true - RZ\Roadiz\Documents\Packages: ~ RZ\Roadiz\Documents\DownscaleImageManager: ~ RZ\Roadiz\Documents\DocumentArchiver: ~ # diff --git a/migrations/Version20201203004857.php b/migrations/Version20201203004857.php index 746639af..48ccc417 100644 --- a/migrations/Version20201203004857.php +++ b/migrations/Version20201203004857.php @@ -7,7 +7,7 @@ use Doctrine\Migrations\AbstractMigration; /** - * Database initialization migration for MySQL/MariaDB. + * Database initialization migration. * * @package RZ\Roadiz\Migrations */ @@ -15,17 +15,22 @@ final class Version20201203004857 extends AbstractMigration { public function getDescription() : string { - return 'Database initialization migration for MySQL/MariaDB.'; + return 'Database initialization migration.'; } public function up(Schema $schema) : void { - $this->skipIf( - $this->connection->getDatabasePlatform()->getName() !== 'mysql', - 'Migration can only be executed safely on \'mysql\'.' - ); $this->skipIf($schema->hasTable('nodes'), 'Database has been initialized before Doctrine Migration tool.'); + if ($this->connection->getDatabasePlatform()->getName() === 'mysql') { + $this->mysqlUp(); + } elseif ($this->connection->getDatabasePlatform()->getName() === 'postgresql') { + $this->postgresUp(); + } + } + + private function mysqlUp(): void + { $this->addSql('CREATE TABLE attribute_group_translations (id INT AUTO_INCREMENT NOT NULL, attribute_group_id INT DEFAULT NULL, translation_id INT DEFAULT NULL, name VARCHAR(255) NOT NULL, INDEX IDX_5C704A6862D643B7 (attribute_group_id), INDEX IDX_5C704A689CAA2B25 (translation_id), INDEX IDX_5C704A685E237E06 (name), UNIQUE INDEX UNIQ_5C704A6862D643B79CAA2B25 (attribute_group_id, translation_id), UNIQUE INDEX UNIQ_5C704A685E237E069CAA2B25 (name, translation_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); $this->addSql('CREATE TABLE attribute_groups (id INT AUTO_INCREMENT NOT NULL, canonical_name VARCHAR(255) NOT NULL, UNIQUE INDEX UNIQ_D28C172A674D812 (canonical_name), INDEX IDX_D28C172A674D812 (canonical_name), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); $this->addSql('CREATE TABLE attribute_translations (id INT AUTO_INCREMENT NOT NULL, attribute_id INT DEFAULT NULL, translation_id INT DEFAULT NULL, label VARCHAR(255) NOT NULL, options LONGTEXT DEFAULT NULL COMMENT \'(DC2Type:simple_array)\', INDEX IDX_4059D4A0B6E62EFA (attribute_id), INDEX IDX_4059D4A09CAA2B25 (translation_id), INDEX IDX_4059D4A0EA750E8 (label), UNIQUE INDEX UNIQ_4059D4A0B6E62EFA9CAA2B25 (attribute_id, translation_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); @@ -133,6 +138,348 @@ public function up(Schema $schema) : void $this->addSql('ALTER TABLE users_groups ADD CONSTRAINT FK_FF8AB7E0FE54D947 FOREIGN KEY (group_id) REFERENCES `groups` (id)'); } + private function postgresUp(): void + { + $this->addSql('CREATE SEQUENCE attribute_group_translations_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE attribute_groups_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE attribute_translations_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE attribute_value_translations_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE attribute_values_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE attributes_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE attributes_documents_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE custom_form_answers_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE custom_form_field_attributes_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE custom_form_fields_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE custom_forms_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE documents_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE documents_translations_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE folders_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE folders_translations_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE log_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE login_attempts_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE node_type_fields_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE node_types_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE nodes_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE nodes_custom_forms_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE nodes_sources_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE nodes_sources_documents_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE nodes_to_nodes_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE redirections_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE roles_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE settings_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE settings_groups_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE tags_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE tags_translations_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE tags_translations_documents_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE translations_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE url_aliases_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE user_log_entries_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE usergroups_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE users_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE TABLE attribute_group_translations (id INT NOT NULL, attribute_group_id INT DEFAULT NULL, translation_id INT DEFAULT NULL, name VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_5C704A6862D643B7 ON attribute_group_translations (attribute_group_id)'); + $this->addSql('CREATE INDEX IDX_5C704A689CAA2B25 ON attribute_group_translations (translation_id)'); + $this->addSql('CREATE INDEX IDX_5C704A685E237E06 ON attribute_group_translations (name)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_5C704A6862D643B79CAA2B25 ON attribute_group_translations (attribute_group_id, translation_id)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_5C704A685E237E069CAA2B25 ON attribute_group_translations (name, translation_id)'); + $this->addSql('CREATE TABLE attribute_groups (id INT NOT NULL, canonical_name VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_D28C172A674D812 ON attribute_groups (canonical_name)'); + $this->addSql('CREATE INDEX IDX_D28C172A674D812 ON attribute_groups (canonical_name)'); + $this->addSql('CREATE TABLE attribute_translations (id INT NOT NULL, attribute_id INT DEFAULT NULL, translation_id INT DEFAULT NULL, label VARCHAR(255) NOT NULL, options TEXT DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_4059D4A0B6E62EFA ON attribute_translations (attribute_id)'); + $this->addSql('CREATE INDEX IDX_4059D4A09CAA2B25 ON attribute_translations (translation_id)'); + $this->addSql('CREATE INDEX IDX_4059D4A0EA750E8 ON attribute_translations (label)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_4059D4A0B6E62EFA9CAA2B25 ON attribute_translations (attribute_id, translation_id)'); + $this->addSql('COMMENT ON COLUMN attribute_translations.options IS \'(DC2Type:simple_array)\''); + $this->addSql('CREATE TABLE attribute_value_translations (id INT NOT NULL, translation_id INT DEFAULT NULL, attribute_value INT DEFAULT NULL, value VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_1293849B9CAA2B25 ON attribute_value_translations (translation_id)'); + $this->addSql('CREATE INDEX IDX_1293849BFE4FBB82 ON attribute_value_translations (attribute_value)'); + $this->addSql('CREATE INDEX IDX_1293849B1D775834 ON attribute_value_translations (value)'); + $this->addSql('CREATE INDEX IDX_1293849B9CAA2B25FE4FBB82 ON attribute_value_translations (translation_id, attribute_value)'); + $this->addSql('CREATE TABLE attribute_values (id INT NOT NULL, attribute_id INT DEFAULT NULL, node_id INT DEFAULT NULL, position DOUBLE PRECISION NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_184662BCB6E62EFA ON attribute_values (attribute_id)'); + $this->addSql('CREATE INDEX IDX_184662BC460D9FD7 ON attribute_values (node_id)'); + $this->addSql('CREATE INDEX IDX_184662BCB6E62EFA460D9FD7 ON attribute_values (attribute_id, node_id)'); + $this->addSql('CREATE TABLE attributes (id INT NOT NULL, group_id INT DEFAULT NULL, code VARCHAR(255) NOT NULL, searchable BOOLEAN DEFAULT \'false\' NOT NULL, type INT NOT NULL, color VARCHAR(7) DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_319B9E7077153098 ON attributes (code)'); + $this->addSql('CREATE INDEX IDX_319B9E7077153098 ON attributes (code)'); + $this->addSql('CREATE INDEX IDX_319B9E708CDE5729 ON attributes (type)'); + $this->addSql('CREATE INDEX IDX_319B9E7094CD8C0D ON attributes (searchable)'); + $this->addSql('CREATE INDEX IDX_319B9E70FE54D947 ON attributes (group_id)'); + $this->addSql('CREATE TABLE attributes_documents (id INT NOT NULL, attribute_id INT DEFAULT NULL, document_id INT DEFAULT NULL, position DOUBLE PRECISION NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_67CCC9E0B6E62EFA ON attributes_documents (attribute_id)'); + $this->addSql('CREATE INDEX IDX_67CCC9E0C33F7837 ON attributes_documents (document_id)'); + $this->addSql('CREATE INDEX IDX_67CCC9E0462CE4F5 ON attributes_documents (position)'); + $this->addSql('CREATE INDEX IDX_67CCC9E0B6E62EFA462CE4F5 ON attributes_documents (attribute_id, position)'); + $this->addSql('CREATE TABLE custom_form_answers (id INT NOT NULL, custom_form_id INT DEFAULT NULL, ip VARCHAR(255) NOT NULL, submitted_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_1A3BB12658AFF2B0 ON custom_form_answers (custom_form_id)'); + $this->addSql('CREATE INDEX IDX_1A3BB126A5E3B32D ON custom_form_answers (ip)'); + $this->addSql('CREATE INDEX IDX_1A3BB1263182C73C ON custom_form_answers (submitted_at)'); + $this->addSql('CREATE TABLE custom_form_field_attributes (id INT NOT NULL, custom_form_answer_id INT DEFAULT NULL, custom_form_field_id INT DEFAULT NULL, value TEXT DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_B7133605F1D6C2D1 ON custom_form_field_attributes (custom_form_answer_id)'); + $this->addSql('CREATE INDEX IDX_B71336057F13CC0F ON custom_form_field_attributes (custom_form_field_id)'); + $this->addSql('CREATE TABLE custom_form_answers_documents (customformfieldattribute_id INT NOT NULL, document_id INT NOT NULL, PRIMARY KEY(customformfieldattribute_id, document_id))'); + $this->addSql('CREATE INDEX IDX_E979F877C84CA2FC ON custom_form_answers_documents (customformfieldattribute_id)'); + $this->addSql('CREATE INDEX IDX_E979F877C33F7837 ON custom_form_answers_documents (document_id)'); + $this->addSql('CREATE TABLE custom_form_fields (id INT NOT NULL, custom_form_id INT DEFAULT NULL, name VARCHAR(255) NOT NULL, label VARCHAR(255) NOT NULL, placeholder VARCHAR(255) DEFAULT NULL, description TEXT DEFAULT NULL, default_values TEXT DEFAULT NULL, type INT NOT NULL, expanded BOOLEAN DEFAULT \'false\' NOT NULL, field_required BOOLEAN DEFAULT \'false\' NOT NULL, group_name VARCHAR(255) DEFAULT NULL, group_name_canonical VARCHAR(255) DEFAULT NULL, position DOUBLE PRECISION NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_4A3782EC58AFF2B0 ON custom_form_fields (custom_form_id)'); + $this->addSql('CREATE INDEX IDX_4A3782EC462CE4F5 ON custom_form_fields (position)'); + $this->addSql('CREATE INDEX IDX_4A3782EC77792576 ON custom_form_fields (group_name)'); + $this->addSql('CREATE INDEX IDX_4A3782EC8CDE5729 ON custom_form_fields (type)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_4A3782EC5E237E0658AFF2B0 ON custom_form_fields (name, custom_form_id)'); + $this->addSql('CREATE TABLE custom_forms (id INT NOT NULL, color VARCHAR(255) DEFAULT NULL, name VARCHAR(255) NOT NULL, display_name VARCHAR(255) NOT NULL, description TEXT DEFAULT NULL, email TEXT DEFAULT NULL, open BOOLEAN DEFAULT \'true\' NOT NULL, close_date TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_3E32E39E5E237E06 ON custom_forms (name)'); + $this->addSql('CREATE INDEX IDX_3E32E39E8B8E8428 ON custom_forms (created_at)'); + $this->addSql('CREATE INDEX IDX_3E32E39E43625D9F ON custom_forms (updated_at)'); + $this->addSql('CREATE TABLE documents (id INT NOT NULL, raw_document INT DEFAULT NULL, original INT DEFAULT NULL, raw BOOLEAN DEFAULT \'false\' NOT NULL, embedId VARCHAR(255) DEFAULT NULL, embedPlatform VARCHAR(255) DEFAULT NULL, filename VARCHAR(255) DEFAULT NULL, mime_type VARCHAR(255) DEFAULT NULL, folder VARCHAR(255) NOT NULL, private BOOLEAN DEFAULT \'false\' NOT NULL, imageWidth INT DEFAULT 0 NOT NULL, imageHeight INT DEFAULT 0 NOT NULL, average_color VARCHAR(7) DEFAULT NULL, filesize INT DEFAULT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_A2B0728826CBD5A5 ON documents (raw_document)'); + $this->addSql('CREATE INDEX IDX_A2B072882F727085 ON documents (original)'); + $this->addSql('CREATE INDEX IDX_A2B072881AB3DB55 ON documents (raw)'); + $this->addSql('CREATE INDEX IDX_A2B07288D206C1D1 ON documents (private)'); + $this->addSql('CREATE INDEX IDX_A2B072881AB3DB55D206C1D1 ON documents (raw, private)'); + $this->addSql('CREATE INDEX IDX_A2B072882100AA2E ON documents (mime_type)'); + $this->addSql('CREATE TABLE documents_translations (id INT NOT NULL, translation_id INT DEFAULT NULL, document_id INT DEFAULT NULL, name VARCHAR(255) DEFAULT NULL, description TEXT DEFAULT NULL, copyright TEXT DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_5CD2F5509CAA2B25 ON documents_translations (translation_id)'); + $this->addSql('CREATE INDEX IDX_5CD2F550C33F7837 ON documents_translations (document_id)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_5CD2F550C33F78379CAA2B25 ON documents_translations (document_id, translation_id)'); + $this->addSql('CREATE TABLE folders (id INT NOT NULL, parent_id INT DEFAULT NULL, folder_name VARCHAR(255) NOT NULL, visible BOOLEAN DEFAULT \'true\' NOT NULL, position DOUBLE PRECISION NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_FE37D30F47BC5813 ON folders (folder_name)'); + $this->addSql('CREATE INDEX IDX_FE37D30F727ACA70 ON folders (parent_id)'); + $this->addSql('CREATE INDEX IDX_FE37D30F7AB0E859 ON folders (visible)'); + $this->addSql('CREATE INDEX IDX_FE37D30F462CE4F5 ON folders (position)'); + $this->addSql('CREATE INDEX IDX_FE37D30F8B8E8428 ON folders (created_at)'); + $this->addSql('CREATE INDEX IDX_FE37D30F43625D9F ON folders (updated_at)'); + $this->addSql('CREATE TABLE documents_folders (folder_id INT NOT NULL, document_id INT NOT NULL, PRIMARY KEY(folder_id, document_id))'); + $this->addSql('CREATE INDEX IDX_617BB29C162CB942 ON documents_folders (folder_id)'); + $this->addSql('CREATE INDEX IDX_617BB29CC33F7837 ON documents_folders (document_id)'); + $this->addSql('CREATE TABLE folders_translations (id INT NOT NULL, folder_id INT DEFAULT NULL, translation_id INT DEFAULT NULL, name VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_9F6A68B2162CB942 ON folders_translations (folder_id)'); + $this->addSql('CREATE INDEX IDX_9F6A68B29CAA2B25 ON folders_translations (translation_id)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_9F6A68B2162CB9429CAA2B25 ON folders_translations (folder_id, translation_id)'); + $this->addSql('CREATE TABLE log (id INT NOT NULL, user_id INT DEFAULT NULL, node_source_id INT DEFAULT NULL, username VARCHAR(255) DEFAULT NULL, message TEXT NOT NULL, level INT NOT NULL, datetime TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, client_ip VARCHAR(255) DEFAULT NULL, channel VARCHAR(255) DEFAULT NULL, additional_data JSON DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_8F3F68C5A76ED395 ON log (user_id)'); + $this->addSql('CREATE INDEX IDX_8F3F68C58E831402 ON log (node_source_id)'); + $this->addSql('CREATE INDEX IDX_8F3F68C593F3C6CA ON log (datetime)'); + $this->addSql('CREATE INDEX IDX_8F3F68C59AEACC13 ON log (level)'); + $this->addSql('CREATE INDEX IDX_8F3F68C5F85E0677 ON log (username)'); + $this->addSql('CREATE INDEX IDX_8F3F68C5A2F98E47 ON log (channel)'); + $this->addSql('CREATE TABLE login_attempts (id INT NOT NULL, ip_address VARCHAR(50) DEFAULT NULL, date TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, blocks_login_until TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, username VARCHAR(255) NOT NULL, attempt_count INT DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_9163C7FBF85E0677 ON login_attempts (username)'); + $this->addSql('CREATE INDEX IDX_9163C7FBEFF8A4EEF85E0677 ON login_attempts (blocks_login_until, username)'); + $this->addSql('CREATE INDEX IDX_9163C7FBEFF8A4EEF85E067722FFD58C ON login_attempts (blocks_login_until, username, ip_address)'); + $this->addSql('COMMENT ON COLUMN login_attempts.date IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('CREATE TABLE node_type_fields (id INT NOT NULL, node_type_id INT DEFAULT NULL, name VARCHAR(255) NOT NULL, label VARCHAR(255) NOT NULL, placeholder VARCHAR(255) DEFAULT NULL, description TEXT DEFAULT NULL, default_values TEXT DEFAULT NULL, type INT NOT NULL, expanded BOOLEAN DEFAULT \'false\' NOT NULL, universal BOOLEAN DEFAULT \'false\' NOT NULL, exclude_from_search BOOLEAN DEFAULT \'false\' NOT NULL, min_length INT DEFAULT NULL, max_length INT DEFAULT NULL, indexed BOOLEAN DEFAULT \'false\' NOT NULL, visible BOOLEAN DEFAULT \'true\' NOT NULL, group_name VARCHAR(255) DEFAULT NULL, group_name_canonical VARCHAR(255) DEFAULT NULL, position DOUBLE PRECISION NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_1D3923596344C9E1 ON node_type_fields (node_type_id)'); + $this->addSql('CREATE INDEX IDX_1D3923597AB0E859 ON node_type_fields (visible)'); + $this->addSql('CREATE INDEX IDX_1D392359D9416D95 ON node_type_fields (indexed)'); + $this->addSql('CREATE INDEX IDX_1D392359462CE4F5 ON node_type_fields (position)'); + $this->addSql('CREATE INDEX IDX_1D39235977792576 ON node_type_fields (group_name)'); + $this->addSql('CREATE INDEX IDX_1D3923594BAF07A4 ON node_type_fields (group_name_canonical)'); + $this->addSql('CREATE INDEX IDX_1D3923598CDE5729 ON node_type_fields (type)'); + $this->addSql('CREATE INDEX IDX_1D392359A4B8F6E1 ON node_type_fields (universal)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_1D3923595E237E066344C9E1 ON node_type_fields (name, node_type_id)'); + $this->addSql('CREATE TABLE node_types (id INT NOT NULL, name VARCHAR(255) NOT NULL, display_name VARCHAR(255) NOT NULL, description TEXT DEFAULT NULL, visible BOOLEAN DEFAULT \'true\' NOT NULL, publishable BOOLEAN DEFAULT \'false\' NOT NULL, reachable BOOLEAN DEFAULT \'true\' NOT NULL, hiding_nodes BOOLEAN DEFAULT \'false\' NOT NULL, hiding_non_reachable_nodes BOOLEAN DEFAULT \'false\' NOT NULL, color VARCHAR(255) DEFAULT NULL, default_ttl INT DEFAULT 0 NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_409B1BCC5E237E06 ON node_types (name)'); + $this->addSql('CREATE INDEX IDX_409B1BCC7AB0E859 ON node_types (visible)'); + $this->addSql('CREATE INDEX IDX_409B1BCC7697C594 ON node_types (publishable)'); + $this->addSql('CREATE INDEX IDX_409B1BCCFB696FF0 ON node_types (hiding_nodes)'); + $this->addSql('CREATE INDEX IDX_409B1BCC5A3C14C7 ON node_types (hiding_non_reachable_nodes)'); + $this->addSql('CREATE INDEX IDX_409B1BCC96ED695F ON node_types (reachable)'); + $this->addSql('CREATE TABLE nodes (id INT NOT NULL, parent_node_id INT DEFAULT NULL, node_name VARCHAR(255) NOT NULL, dynamic_node_name BOOLEAN DEFAULT \'true\' NOT NULL, home BOOLEAN DEFAULT \'false\' NOT NULL, visible BOOLEAN DEFAULT \'true\' NOT NULL, status INT NOT NULL, ttl INT DEFAULT 0 NOT NULL, locked BOOLEAN DEFAULT \'false\' NOT NULL, priority NUMERIC(2, 1) NOT NULL, hide_children BOOLEAN DEFAULT \'false\' NOT NULL, sterile BOOLEAN DEFAULT \'false\' NOT NULL, children_order VARCHAR(255) NOT NULL, children_order_direction VARCHAR(4) NOT NULL, position DOUBLE PRECISION NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, nodeType_id INT DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_1D3D05FC9987F390 ON nodes (node_name)'); + $this->addSql('CREATE INDEX IDX_1D3D05FC47D04729 ON nodes (nodeType_id)'); + $this->addSql('CREATE INDEX IDX_1D3D05FC3445EB91 ON nodes (parent_node_id)'); + $this->addSql('CREATE INDEX IDX_1D3D05FC7AB0E859 ON nodes (visible)'); + $this->addSql('CREATE INDEX IDX_1D3D05FC7B00651C ON nodes (status)'); + $this->addSql('CREATE INDEX IDX_1D3D05FCEAD2C891 ON nodes (locked)'); + $this->addSql('CREATE INDEX IDX_1D3D05FCF32D8BE6 ON nodes (sterile)'); + $this->addSql('CREATE INDEX IDX_1D3D05FC462CE4F5 ON nodes (position)'); + $this->addSql('CREATE INDEX IDX_1D3D05FC8B8E8428 ON nodes (created_at)'); + $this->addSql('CREATE INDEX IDX_1D3D05FC43625D9F ON nodes (updated_at)'); + $this->addSql('CREATE INDEX IDX_1D3D05FC50E2D3D2 ON nodes (hide_children)'); + $this->addSql('CREATE INDEX IDX_1D3D05FC9987F3907B00651C ON nodes (node_name, status)'); + $this->addSql('CREATE INDEX IDX_1D3D05FC7AB0E8597B00651C ON nodes (visible, status)'); + $this->addSql('CREATE INDEX IDX_1D3D05FC7AB0E8597B00651C3445EB91 ON nodes (visible, status, parent_node_id)'); + $this->addSql('CREATE INDEX IDX_1D3D05FC7AB0E8593445EB91 ON nodes (visible, parent_node_id)'); + $this->addSql('CREATE INDEX IDX_1D3D05FC71D60CD0 ON nodes (home)'); + $this->addSql('CREATE TABLE nodes_tags (node_id INT NOT NULL, tag_id INT NOT NULL, PRIMARY KEY(node_id, tag_id))'); + $this->addSql('CREATE INDEX IDX_5B5CB38C460D9FD7 ON nodes_tags (node_id)'); + $this->addSql('CREATE INDEX IDX_5B5CB38CBAD26311 ON nodes_tags (tag_id)'); + $this->addSql('CREATE TABLE stack_types (node_id INT NOT NULL, nodetype_id INT NOT NULL, PRIMARY KEY(node_id, nodetype_id))'); + $this->addSql('CREATE INDEX IDX_DE24E53460D9FD7 ON stack_types (node_id)'); + $this->addSql('CREATE INDEX IDX_DE24E53886D7EB5 ON stack_types (nodetype_id)'); + $this->addSql('CREATE TABLE nodes_custom_forms (id INT NOT NULL, node_id INT DEFAULT NULL, custom_form_id INT DEFAULT NULL, node_type_field_id INT DEFAULT NULL, position DOUBLE PRECISION NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_4D401A0C460D9FD7 ON nodes_custom_forms (node_id)'); + $this->addSql('CREATE INDEX IDX_4D401A0C58AFF2B0 ON nodes_custom_forms (custom_form_id)'); + $this->addSql('CREATE INDEX IDX_4D401A0C47705282 ON nodes_custom_forms (node_type_field_id)'); + $this->addSql('CREATE INDEX IDX_4D401A0C462CE4F5 ON nodes_custom_forms (position)'); + $this->addSql('CREATE TABLE nodes_sources (id INT NOT NULL, node_id INT DEFAULT NULL, translation_id INT DEFAULT NULL, title VARCHAR(255) DEFAULT NULL, published_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, meta_title VARCHAR(255) NOT NULL, meta_keywords TEXT NOT NULL, meta_description TEXT NOT NULL, discr VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_7C7DED6D460D9FD7 ON nodes_sources (node_id)'); + $this->addSql('CREATE INDEX IDX_7C7DED6D9CAA2B25 ON nodes_sources (translation_id)'); + $this->addSql('CREATE INDEX IDX_7C7DED6D4AD26064 ON nodes_sources (discr)'); + $this->addSql('CREATE INDEX IDX_7C7DED6D4AD260649CAA2B25 ON nodes_sources (discr, translation_id)'); + $this->addSql('CREATE INDEX IDX_7C7DED6DE0D4FDE14AD260649CAA2B25 ON nodes_sources (published_at, discr, translation_id)'); + $this->addSql('CREATE INDEX IDX_7C7DED6D2B36786B ON nodes_sources (title)'); + $this->addSql('CREATE INDEX IDX_7C7DED6DE0D4FDE1 ON nodes_sources (published_at)'); + $this->addSql('CREATE INDEX IDX_7C7DED6DE0D4FDE19CAA2B25 ON nodes_sources (published_at, translation_id)'); + $this->addSql('CREATE INDEX IDX_7C7DED6D460D9FD79CAA2B25E0D4FDE1 ON nodes_sources (node_id, translation_id, published_at)'); + $this->addSql('CREATE INDEX IDX_7C7DED6D2B36786BE0D4FDE1 ON nodes_sources (title, published_at)'); + $this->addSql('CREATE INDEX IDX_7C7DED6D2B36786BE0D4FDE19CAA2B25 ON nodes_sources (title, published_at, translation_id)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_7C7DED6D460D9FD79CAA2B25 ON nodes_sources (node_id, translation_id)'); + $this->addSql('CREATE TABLE nodes_sources_documents (id INT NOT NULL, ns_id INT DEFAULT NULL, document_id INT DEFAULT NULL, node_type_field_id INT DEFAULT NULL, position DOUBLE PRECISION NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_1CD104F7AA2D61 ON nodes_sources_documents (ns_id)'); + $this->addSql('CREATE INDEX IDX_1CD104F7C33F7837 ON nodes_sources_documents (document_id)'); + $this->addSql('CREATE INDEX IDX_1CD104F747705282 ON nodes_sources_documents (node_type_field_id)'); + $this->addSql('CREATE INDEX IDX_1CD104F7462CE4F5 ON nodes_sources_documents (position)'); + $this->addSql('CREATE INDEX IDX_1CD104F7AA2D6147705282 ON nodes_sources_documents (ns_id, node_type_field_id)'); + $this->addSql('CREATE INDEX IDX_1CD104F7AA2D6147705282462CE4F5 ON nodes_sources_documents (ns_id, node_type_field_id, position)'); + $this->addSql('CREATE TABLE nodes_to_nodes (id INT NOT NULL, node_a_id INT DEFAULT NULL, node_b_id INT DEFAULT NULL, node_type_field_id INT DEFAULT NULL, position DOUBLE PRECISION NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_761F9A91FC7ADECE ON nodes_to_nodes (node_a_id)'); + $this->addSql('CREATE INDEX IDX_761F9A91EECF7120 ON nodes_to_nodes (node_b_id)'); + $this->addSql('CREATE INDEX IDX_761F9A9147705282 ON nodes_to_nodes (node_type_field_id)'); + $this->addSql('CREATE INDEX IDX_761F9A91462CE4F5 ON nodes_to_nodes (position)'); + $this->addSql('CREATE INDEX IDX_761F9A91FC7ADECE47705282 ON nodes_to_nodes (node_a_id, node_type_field_id)'); + $this->addSql('CREATE INDEX IDX_761F9A91FC7ADECE47705282462CE4F5 ON nodes_to_nodes (node_a_id, node_type_field_id, position)'); + $this->addSql('CREATE INDEX IDX_761F9A91EECF712047705282 ON nodes_to_nodes (node_b_id, node_type_field_id)'); + $this->addSql('CREATE INDEX IDX_761F9A91EECF712047705282462CE4F5 ON nodes_to_nodes (node_b_id, node_type_field_id, position)'); + $this->addSql('CREATE TABLE redirections (id INT NOT NULL, ns_id INT DEFAULT NULL, query VARCHAR(255) NOT NULL, redirectUri VARCHAR(255) DEFAULT NULL, type INT NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_38F5ECE424BDB5EB ON redirections (query)'); + $this->addSql('CREATE INDEX IDX_38F5ECE4AA2D61 ON redirections (ns_id)'); + $this->addSql('CREATE INDEX IDX_38F5ECE48B8E8428 ON redirections (created_at)'); + $this->addSql('CREATE INDEX IDX_38F5ECE443625D9F ON redirections (updated_at)'); + $this->addSql('CREATE TABLE roles (id INT NOT NULL, name VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_B63E2EC75E237E06 ON roles (name)'); + $this->addSql('CREATE TABLE settings (id INT NOT NULL, setting_group_id INT DEFAULT NULL, name VARCHAR(255) NOT NULL, description TEXT DEFAULT NULL, value TEXT DEFAULT NULL, visible BOOLEAN DEFAULT \'true\' NOT NULL, encrypted BOOLEAN DEFAULT \'false\' NOT NULL, type INT NOT NULL, defaultValues TEXT DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_E545A0C55E237E06 ON settings (name)'); + $this->addSql('CREATE INDEX IDX_E545A0C550DDE1BD ON settings (setting_group_id)'); + $this->addSql('CREATE INDEX IDX_E545A0C58CDE5729 ON settings (type)'); + $this->addSql('CREATE INDEX IDX_E545A0C55E237E06 ON settings (name)'); + $this->addSql('CREATE INDEX IDX_E545A0C57AB0E859 ON settings (visible)'); + $this->addSql('CREATE TABLE settings_groups (id INT NOT NULL, name VARCHAR(255) NOT NULL, in_menu BOOLEAN DEFAULT \'false\' NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_FFD519025E237E06 ON settings_groups (name)'); + $this->addSql('CREATE TABLE tags (id INT NOT NULL, parent_tag_id INT DEFAULT NULL, color VARCHAR(7) DEFAULT \'#000000\' NOT NULL, tag_name VARCHAR(255) NOT NULL, visible BOOLEAN DEFAULT \'true\' NOT NULL, children_order VARCHAR(255) DEFAULT \'position\' NOT NULL, children_order_direction VARCHAR(4) DEFAULT \'ASC\' NOT NULL, locked BOOLEAN DEFAULT \'false\' NOT NULL, position DOUBLE PRECISION NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_6FBC9426B02CC1B0 ON tags (tag_name)'); + $this->addSql('CREATE INDEX IDX_6FBC9426F5C1A0D7 ON tags (parent_tag_id)'); + $this->addSql('CREATE INDEX IDX_6FBC94267AB0E859 ON tags (visible)'); + $this->addSql('CREATE INDEX IDX_6FBC9426EAD2C891 ON tags (locked)'); + $this->addSql('CREATE INDEX IDX_6FBC9426462CE4F5 ON tags (position)'); + $this->addSql('CREATE INDEX IDX_6FBC94268B8E8428 ON tags (created_at)'); + $this->addSql('CREATE INDEX IDX_6FBC942643625D9F ON tags (updated_at)'); + $this->addSql('CREATE INDEX IDX_6FBC9426F5C1A0D77AB0E859 ON tags (parent_tag_id, visible)'); + $this->addSql('CREATE TABLE tags_translations (id INT NOT NULL, tag_id INT DEFAULT NULL, translation_id INT DEFAULT NULL, name VARCHAR(255) NOT NULL, description TEXT DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_95D326DCBAD26311 ON tags_translations (tag_id)'); + $this->addSql('CREATE INDEX IDX_95D326DC9CAA2B25 ON tags_translations (translation_id)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_95D326DCBAD263119CAA2B25 ON tags_translations (tag_id, translation_id)'); + $this->addSql('CREATE TABLE tags_translations_documents (id INT NOT NULL, tag_translation_id INT DEFAULT NULL, document_id INT DEFAULT NULL, position DOUBLE PRECISION NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_6E886F1F22010F1 ON tags_translations_documents (tag_translation_id)'); + $this->addSql('CREATE INDEX IDX_6E886F1FC33F7837 ON tags_translations_documents (document_id)'); + $this->addSql('CREATE INDEX IDX_6E886F1F462CE4F5 ON tags_translations_documents (position)'); + $this->addSql('CREATE INDEX IDX_6E886F1F22010F1462CE4F5 ON tags_translations_documents (tag_translation_id, position)'); + $this->addSql('CREATE TABLE translations (id INT NOT NULL, locale VARCHAR(10) NOT NULL, override_locale VARCHAR(10) DEFAULT NULL, name VARCHAR(255) NOT NULL, default_translation BOOLEAN DEFAULT \'false\' NOT NULL, available BOOLEAN DEFAULT \'true\' NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_C6B7DA874180C698 ON translations (locale)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_C6B7DA873F824FD6 ON translations (override_locale)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_C6B7DA875E237E06 ON translations (name)'); + $this->addSql('CREATE INDEX IDX_C6B7DA87A58FA485 ON translations (available)'); + $this->addSql('CREATE INDEX IDX_C6B7DA87609A56D9 ON translations (default_translation)'); + $this->addSql('CREATE INDEX IDX_C6B7DA878B8E8428 ON translations (created_at)'); + $this->addSql('CREATE INDEX IDX_C6B7DA8743625D9F ON translations (updated_at)'); + $this->addSql('CREATE INDEX IDX_C6B7DA87A58FA485609A56D9 ON translations (available, default_translation)'); + $this->addSql('CREATE INDEX IDX_C6B7DA87A58FA4854180C698 ON translations (available, locale)'); + $this->addSql('CREATE INDEX IDX_C6B7DA87A58FA4853F824FD6 ON translations (available, override_locale)'); + $this->addSql('CREATE TABLE url_aliases (id INT NOT NULL, ns_id INT DEFAULT NULL, alias VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_E261ED65E16C6B94 ON url_aliases (alias)'); + $this->addSql('CREATE INDEX IDX_E261ED65AA2D61 ON url_aliases (ns_id)'); + $this->addSql('CREATE TABLE user_log_entries (id INT NOT NULL, user_id INT DEFAULT NULL, action VARCHAR(8) NOT NULL, logged_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, object_id VARCHAR(64) DEFAULT NULL, object_class VARCHAR(191) NOT NULL, version INT NOT NULL, data TEXT DEFAULT NULL, username VARCHAR(191) DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_BC2E42C7A76ED395 ON user_log_entries (user_id)'); + $this->addSql('CREATE INDEX log_class_lookup_idx ON user_log_entries (object_class)'); + $this->addSql('CREATE INDEX log_date_lookup_idx ON user_log_entries (logged_at)'); + $this->addSql('CREATE INDEX log_user_lookup_idx ON user_log_entries (username)'); + $this->addSql('CREATE INDEX log_version_lookup_idx ON user_log_entries (object_id, object_class, version)'); + $this->addSql('COMMENT ON COLUMN user_log_entries.data IS \'(DC2Type:array)\''); + $this->addSql('CREATE TABLE usergroups (id INT NOT NULL, name VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_98972EB45E237E06 ON usergroups (name)'); + $this->addSql('CREATE TABLE groups_roles (group_id INT NOT NULL, role_id INT NOT NULL, PRIMARY KEY(group_id, role_id))'); + $this->addSql('CREATE INDEX IDX_E79D4963FE54D947 ON groups_roles (group_id)'); + $this->addSql('CREATE INDEX IDX_E79D4963D60322AC ON groups_roles (role_id)'); + $this->addSql('CREATE TABLE users (id INT NOT NULL, chroot_id INT DEFAULT NULL, facebook_name VARCHAR(255) DEFAULT NULL, picture_url TEXT DEFAULT NULL, enabled BOOLEAN DEFAULT \'true\' NOT NULL, confirmation_token VARCHAR(255) DEFAULT NULL, password_requested_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, username VARCHAR(255) NOT NULL, salt VARCHAR(255) NOT NULL, password VARCHAR(255) NOT NULL, last_login TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, expired BOOLEAN DEFAULT \'false\' NOT NULL, locked BOOLEAN DEFAULT \'false\' NOT NULL, credentials_expires_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, credentials_expired BOOLEAN DEFAULT \'false\' NOT NULL, expires_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, locale VARCHAR(7) DEFAULT NULL, email VARCHAR(255) NOT NULL, firstName VARCHAR(255) DEFAULT NULL, lastName VARCHAR(255) DEFAULT NULL, phone VARCHAR(255) DEFAULT NULL, company VARCHAR(255) DEFAULT NULL, job VARCHAR(255) DEFAULT NULL, birthday TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_1483A5E9C05FB297 ON users (confirmation_token)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_1483A5E9F85E0677 ON users (username)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_1483A5E9E7927C74 ON users (email)'); + $this->addSql('CREATE INDEX IDX_1483A5E96483A539 ON users (chroot_id)'); + $this->addSql('CREATE INDEX IDX_1483A5E950F9BB84 ON users (enabled)'); + $this->addSql('CREATE INDEX IDX_1483A5E9194FED4B ON users (expired)'); + $this->addSql('CREATE INDEX IDX_1483A5E9F9D83E2 ON users (expires_at)'); + $this->addSql('CREATE INDEX IDX_1483A5E94180C698 ON users (locale)'); + $this->addSql('CREATE TABLE users_roles (user_id INT NOT NULL, role_id INT NOT NULL, PRIMARY KEY(user_id, role_id))'); + $this->addSql('CREATE INDEX IDX_51498A8EA76ED395 ON users_roles (user_id)'); + $this->addSql('CREATE INDEX IDX_51498A8ED60322AC ON users_roles (role_id)'); + $this->addSql('CREATE TABLE users_groups (user_id INT NOT NULL, group_id INT NOT NULL, PRIMARY KEY(user_id, group_id))'); + $this->addSql('CREATE INDEX IDX_FF8AB7E0A76ED395 ON users_groups (user_id)'); + $this->addSql('CREATE INDEX IDX_FF8AB7E0FE54D947 ON users_groups (group_id)'); + $this->addSql('ALTER TABLE attribute_group_translations ADD CONSTRAINT FK_5C704A6862D643B7 FOREIGN KEY (attribute_group_id) REFERENCES attribute_groups (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE attribute_group_translations ADD CONSTRAINT FK_5C704A689CAA2B25 FOREIGN KEY (translation_id) REFERENCES translations (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE attribute_translations ADD CONSTRAINT FK_4059D4A0B6E62EFA FOREIGN KEY (attribute_id) REFERENCES attributes (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE attribute_translations ADD CONSTRAINT FK_4059D4A09CAA2B25 FOREIGN KEY (translation_id) REFERENCES translations (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE attribute_value_translations ADD CONSTRAINT FK_1293849B9CAA2B25 FOREIGN KEY (translation_id) REFERENCES translations (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE attribute_value_translations ADD CONSTRAINT FK_1293849BFE4FBB82 FOREIGN KEY (attribute_value) REFERENCES attribute_values (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE attribute_values ADD CONSTRAINT FK_184662BCB6E62EFA FOREIGN KEY (attribute_id) REFERENCES attributes (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE attribute_values ADD CONSTRAINT FK_184662BC460D9FD7 FOREIGN KEY (node_id) REFERENCES nodes (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE attributes ADD CONSTRAINT FK_319B9E70FE54D947 FOREIGN KEY (group_id) REFERENCES attribute_groups (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE attributes_documents ADD CONSTRAINT FK_67CCC9E0B6E62EFA FOREIGN KEY (attribute_id) REFERENCES attributes (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE attributes_documents ADD CONSTRAINT FK_67CCC9E0C33F7837 FOREIGN KEY (document_id) REFERENCES documents (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE custom_form_answers ADD CONSTRAINT FK_1A3BB12658AFF2B0 FOREIGN KEY (custom_form_id) REFERENCES custom_forms (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE custom_form_field_attributes ADD CONSTRAINT FK_B7133605F1D6C2D1 FOREIGN KEY (custom_form_answer_id) REFERENCES custom_form_answers (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE custom_form_field_attributes ADD CONSTRAINT FK_B71336057F13CC0F FOREIGN KEY (custom_form_field_id) REFERENCES custom_form_fields (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE custom_form_answers_documents ADD CONSTRAINT FK_E979F877C84CA2FC FOREIGN KEY (customformfieldattribute_id) REFERENCES custom_form_field_attributes (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE custom_form_answers_documents ADD CONSTRAINT FK_E979F877C33F7837 FOREIGN KEY (document_id) REFERENCES documents (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE custom_form_fields ADD CONSTRAINT FK_4A3782EC58AFF2B0 FOREIGN KEY (custom_form_id) REFERENCES custom_forms (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE documents ADD CONSTRAINT FK_A2B0728826CBD5A5 FOREIGN KEY (raw_document) REFERENCES documents (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE documents ADD CONSTRAINT FK_A2B072882F727085 FOREIGN KEY (original) REFERENCES documents (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE documents_translations ADD CONSTRAINT FK_5CD2F5509CAA2B25 FOREIGN KEY (translation_id) REFERENCES translations (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE documents_translations ADD CONSTRAINT FK_5CD2F550C33F7837 FOREIGN KEY (document_id) REFERENCES documents (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE folders ADD CONSTRAINT FK_FE37D30F727ACA70 FOREIGN KEY (parent_id) REFERENCES folders (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE documents_folders ADD CONSTRAINT FK_617BB29C162CB942 FOREIGN KEY (folder_id) REFERENCES folders (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE documents_folders ADD CONSTRAINT FK_617BB29CC33F7837 FOREIGN KEY (document_id) REFERENCES documents (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE folders_translations ADD CONSTRAINT FK_9F6A68B2162CB942 FOREIGN KEY (folder_id) REFERENCES folders (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE folders_translations ADD CONSTRAINT FK_9F6A68B29CAA2B25 FOREIGN KEY (translation_id) REFERENCES translations (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE log ADD CONSTRAINT FK_8F3F68C5A76ED395 FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE log ADD CONSTRAINT FK_8F3F68C58E831402 FOREIGN KEY (node_source_id) REFERENCES nodes_sources (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE node_type_fields ADD CONSTRAINT FK_1D3923596344C9E1 FOREIGN KEY (node_type_id) REFERENCES node_types (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE nodes ADD CONSTRAINT FK_1D3D05FC47D04729 FOREIGN KEY (nodeType_id) REFERENCES node_types (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE nodes ADD CONSTRAINT FK_1D3D05FC3445EB91 FOREIGN KEY (parent_node_id) REFERENCES nodes (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE nodes_tags ADD CONSTRAINT FK_5B5CB38C460D9FD7 FOREIGN KEY (node_id) REFERENCES nodes (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE nodes_tags ADD CONSTRAINT FK_5B5CB38CBAD26311 FOREIGN KEY (tag_id) REFERENCES tags (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE stack_types ADD CONSTRAINT FK_DE24E53460D9FD7 FOREIGN KEY (node_id) REFERENCES nodes (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE stack_types ADD CONSTRAINT FK_DE24E53886D7EB5 FOREIGN KEY (nodetype_id) REFERENCES node_types (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE nodes_custom_forms ADD CONSTRAINT FK_4D401A0C460D9FD7 FOREIGN KEY (node_id) REFERENCES nodes (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE nodes_custom_forms ADD CONSTRAINT FK_4D401A0C58AFF2B0 FOREIGN KEY (custom_form_id) REFERENCES custom_forms (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE nodes_custom_forms ADD CONSTRAINT FK_4D401A0C47705282 FOREIGN KEY (node_type_field_id) REFERENCES node_type_fields (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE nodes_sources ADD CONSTRAINT FK_7C7DED6D460D9FD7 FOREIGN KEY (node_id) REFERENCES nodes (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE nodes_sources ADD CONSTRAINT FK_7C7DED6D9CAA2B25 FOREIGN KEY (translation_id) REFERENCES translations (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE nodes_sources_documents ADD CONSTRAINT FK_1CD104F7AA2D61 FOREIGN KEY (ns_id) REFERENCES nodes_sources (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE nodes_sources_documents ADD CONSTRAINT FK_1CD104F7C33F7837 FOREIGN KEY (document_id) REFERENCES documents (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE nodes_sources_documents ADD CONSTRAINT FK_1CD104F747705282 FOREIGN KEY (node_type_field_id) REFERENCES node_type_fields (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE nodes_to_nodes ADD CONSTRAINT FK_761F9A91FC7ADECE FOREIGN KEY (node_a_id) REFERENCES nodes (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE nodes_to_nodes ADD CONSTRAINT FK_761F9A91EECF7120 FOREIGN KEY (node_b_id) REFERENCES nodes (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE nodes_to_nodes ADD CONSTRAINT FK_761F9A9147705282 FOREIGN KEY (node_type_field_id) REFERENCES node_type_fields (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE redirections ADD CONSTRAINT FK_38F5ECE4AA2D61 FOREIGN KEY (ns_id) REFERENCES nodes_sources (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE settings ADD CONSTRAINT FK_E545A0C550DDE1BD FOREIGN KEY (setting_group_id) REFERENCES settings_groups (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE tags ADD CONSTRAINT FK_6FBC9426F5C1A0D7 FOREIGN KEY (parent_tag_id) REFERENCES tags (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE tags_translations ADD CONSTRAINT FK_95D326DCBAD26311 FOREIGN KEY (tag_id) REFERENCES tags (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE tags_translations ADD CONSTRAINT FK_95D326DC9CAA2B25 FOREIGN KEY (translation_id) REFERENCES translations (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE tags_translations_documents ADD CONSTRAINT FK_6E886F1F22010F1 FOREIGN KEY (tag_translation_id) REFERENCES tags_translations (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE tags_translations_documents ADD CONSTRAINT FK_6E886F1FC33F7837 FOREIGN KEY (document_id) REFERENCES documents (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE url_aliases ADD CONSTRAINT FK_E261ED65AA2D61 FOREIGN KEY (ns_id) REFERENCES nodes_sources (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE user_log_entries ADD CONSTRAINT FK_BC2E42C7A76ED395 FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE groups_roles ADD CONSTRAINT FK_E79D4963FE54D947 FOREIGN KEY (group_id) REFERENCES usergroups (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE groups_roles ADD CONSTRAINT FK_E79D4963D60322AC FOREIGN KEY (role_id) REFERENCES roles (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE users ADD CONSTRAINT FK_1483A5E96483A539 FOREIGN KEY (chroot_id) REFERENCES nodes (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE users_roles ADD CONSTRAINT FK_51498A8EA76ED395 FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE users_roles ADD CONSTRAINT FK_51498A8ED60322AC FOREIGN KEY (role_id) REFERENCES roles (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE users_groups ADD CONSTRAINT FK_FF8AB7E0A76ED395 FOREIGN KEY (user_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE users_groups ADD CONSTRAINT FK_FF8AB7E0FE54D947 FOREIGN KEY (group_id) REFERENCES usergroups (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + } + public function down(Schema $schema) : void { $this->throwIrreversibleMigrationException(); diff --git a/migrations/Version20201225181256.php b/migrations/Version20201225181256.php index 5c22be4b..3e099267 100644 --- a/migrations/Version20201225181256.php +++ b/migrations/Version20201225181256.php @@ -9,361 +9,19 @@ /** * Database initialization migration for PostgreSQL. * + * @deprecated Use Roadiz\Core\Migrations\Version20201203004857 instead. * @package RZ\Roadiz\Migrations */ final class Version20201225181256 extends AbstractMigration { public function getDescription() : string { - return 'Database initialization migration for PostgreSQL.'; + return '[deprecated] Database initialization migration for PostgreSQL.'; } public function up(Schema $schema) : void { - $this->skipIf( - $this->connection->getDatabasePlatform()->getName() !== 'postgresql', - 'Migration can only be executed safely on \'postgresql\'.' - ); - $this->skipIf($schema->hasTable('nodes'), 'Database has been initialized before Doctrine Migration tool.'); - - $this->addSql('CREATE SEQUENCE attribute_group_translations_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE attribute_groups_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE attribute_translations_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE attribute_value_translations_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE attribute_values_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE attributes_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE attributes_documents_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE custom_form_answers_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE custom_form_field_attributes_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE custom_form_fields_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE custom_forms_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE documents_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE documents_translations_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE folders_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE folders_translations_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE log_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE login_attempts_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE node_type_fields_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE node_types_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE nodes_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE nodes_custom_forms_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE nodes_sources_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE nodes_sources_documents_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE nodes_to_nodes_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE redirections_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE roles_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE settings_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE settings_groups_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE tags_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE tags_translations_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE tags_translations_documents_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE translations_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE url_aliases_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE user_log_entries_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE usergroups_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE SEQUENCE users_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); - $this->addSql('CREATE TABLE attribute_group_translations (id INT NOT NULL, attribute_group_id INT DEFAULT NULL, translation_id INT DEFAULT NULL, name VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE INDEX IDX_5C704A6862D643B7 ON attribute_group_translations (attribute_group_id)'); - $this->addSql('CREATE INDEX IDX_5C704A689CAA2B25 ON attribute_group_translations (translation_id)'); - $this->addSql('CREATE INDEX IDX_5C704A685E237E06 ON attribute_group_translations (name)'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_5C704A6862D643B79CAA2B25 ON attribute_group_translations (attribute_group_id, translation_id)'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_5C704A685E237E069CAA2B25 ON attribute_group_translations (name, translation_id)'); - $this->addSql('CREATE TABLE attribute_groups (id INT NOT NULL, canonical_name VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_D28C172A674D812 ON attribute_groups (canonical_name)'); - $this->addSql('CREATE INDEX IDX_D28C172A674D812 ON attribute_groups (canonical_name)'); - $this->addSql('CREATE TABLE attribute_translations (id INT NOT NULL, attribute_id INT DEFAULT NULL, translation_id INT DEFAULT NULL, label VARCHAR(255) NOT NULL, options TEXT DEFAULT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE INDEX IDX_4059D4A0B6E62EFA ON attribute_translations (attribute_id)'); - $this->addSql('CREATE INDEX IDX_4059D4A09CAA2B25 ON attribute_translations (translation_id)'); - $this->addSql('CREATE INDEX IDX_4059D4A0EA750E8 ON attribute_translations (label)'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_4059D4A0B6E62EFA9CAA2B25 ON attribute_translations (attribute_id, translation_id)'); - $this->addSql('COMMENT ON COLUMN attribute_translations.options IS \'(DC2Type:simple_array)\''); - $this->addSql('CREATE TABLE attribute_value_translations (id INT NOT NULL, translation_id INT DEFAULT NULL, attribute_value INT DEFAULT NULL, value VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE INDEX IDX_1293849B9CAA2B25 ON attribute_value_translations (translation_id)'); - $this->addSql('CREATE INDEX IDX_1293849BFE4FBB82 ON attribute_value_translations (attribute_value)'); - $this->addSql('CREATE INDEX IDX_1293849B1D775834 ON attribute_value_translations (value)'); - $this->addSql('CREATE INDEX IDX_1293849B9CAA2B25FE4FBB82 ON attribute_value_translations (translation_id, attribute_value)'); - $this->addSql('CREATE TABLE attribute_values (id INT NOT NULL, attribute_id INT DEFAULT NULL, node_id INT DEFAULT NULL, position DOUBLE PRECISION NOT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE INDEX IDX_184662BCB6E62EFA ON attribute_values (attribute_id)'); - $this->addSql('CREATE INDEX IDX_184662BC460D9FD7 ON attribute_values (node_id)'); - $this->addSql('CREATE INDEX IDX_184662BCB6E62EFA460D9FD7 ON attribute_values (attribute_id, node_id)'); - $this->addSql('CREATE TABLE attributes (id INT NOT NULL, group_id INT DEFAULT NULL, code VARCHAR(255) NOT NULL, searchable BOOLEAN DEFAULT \'false\' NOT NULL, type INT NOT NULL, color VARCHAR(7) DEFAULT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_319B9E7077153098 ON attributes (code)'); - $this->addSql('CREATE INDEX IDX_319B9E7077153098 ON attributes (code)'); - $this->addSql('CREATE INDEX IDX_319B9E708CDE5729 ON attributes (type)'); - $this->addSql('CREATE INDEX IDX_319B9E7094CD8C0D ON attributes (searchable)'); - $this->addSql('CREATE INDEX IDX_319B9E70FE54D947 ON attributes (group_id)'); - $this->addSql('CREATE TABLE attributes_documents (id INT NOT NULL, attribute_id INT DEFAULT NULL, document_id INT DEFAULT NULL, position DOUBLE PRECISION NOT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE INDEX IDX_67CCC9E0B6E62EFA ON attributes_documents (attribute_id)'); - $this->addSql('CREATE INDEX IDX_67CCC9E0C33F7837 ON attributes_documents (document_id)'); - $this->addSql('CREATE INDEX IDX_67CCC9E0462CE4F5 ON attributes_documents (position)'); - $this->addSql('CREATE INDEX IDX_67CCC9E0B6E62EFA462CE4F5 ON attributes_documents (attribute_id, position)'); - $this->addSql('CREATE TABLE custom_form_answers (id INT NOT NULL, custom_form_id INT DEFAULT NULL, ip VARCHAR(255) NOT NULL, submitted_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE INDEX IDX_1A3BB12658AFF2B0 ON custom_form_answers (custom_form_id)'); - $this->addSql('CREATE INDEX IDX_1A3BB126A5E3B32D ON custom_form_answers (ip)'); - $this->addSql('CREATE INDEX IDX_1A3BB1263182C73C ON custom_form_answers (submitted_at)'); - $this->addSql('CREATE TABLE custom_form_field_attributes (id INT NOT NULL, custom_form_answer_id INT DEFAULT NULL, custom_form_field_id INT DEFAULT NULL, value TEXT DEFAULT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE INDEX IDX_B7133605F1D6C2D1 ON custom_form_field_attributes (custom_form_answer_id)'); - $this->addSql('CREATE INDEX IDX_B71336057F13CC0F ON custom_form_field_attributes (custom_form_field_id)'); - $this->addSql('CREATE TABLE custom_form_answers_documents (customformfieldattribute_id INT NOT NULL, document_id INT NOT NULL, PRIMARY KEY(customformfieldattribute_id, document_id))'); - $this->addSql('CREATE INDEX IDX_E979F877C84CA2FC ON custom_form_answers_documents (customformfieldattribute_id)'); - $this->addSql('CREATE INDEX IDX_E979F877C33F7837 ON custom_form_answers_documents (document_id)'); - $this->addSql('CREATE TABLE custom_form_fields (id INT NOT NULL, custom_form_id INT DEFAULT NULL, name VARCHAR(255) NOT NULL, label VARCHAR(255) NOT NULL, placeholder VARCHAR(255) DEFAULT NULL, description TEXT DEFAULT NULL, default_values TEXT DEFAULT NULL, type INT NOT NULL, expanded BOOLEAN DEFAULT \'false\' NOT NULL, field_required BOOLEAN DEFAULT \'false\' NOT NULL, group_name VARCHAR(255) DEFAULT NULL, group_name_canonical VARCHAR(255) DEFAULT NULL, position DOUBLE PRECISION NOT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE INDEX IDX_4A3782EC58AFF2B0 ON custom_form_fields (custom_form_id)'); - $this->addSql('CREATE INDEX IDX_4A3782EC462CE4F5 ON custom_form_fields (position)'); - $this->addSql('CREATE INDEX IDX_4A3782EC77792576 ON custom_form_fields (group_name)'); - $this->addSql('CREATE INDEX IDX_4A3782EC8CDE5729 ON custom_form_fields (type)'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_4A3782EC5E237E0658AFF2B0 ON custom_form_fields (name, custom_form_id)'); - $this->addSql('CREATE TABLE custom_forms (id INT NOT NULL, color VARCHAR(255) DEFAULT NULL, name VARCHAR(255) NOT NULL, display_name VARCHAR(255) NOT NULL, description TEXT DEFAULT NULL, email TEXT DEFAULT NULL, open BOOLEAN DEFAULT \'true\' NOT NULL, close_date TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_3E32E39E5E237E06 ON custom_forms (name)'); - $this->addSql('CREATE INDEX IDX_3E32E39E8B8E8428 ON custom_forms (created_at)'); - $this->addSql('CREATE INDEX IDX_3E32E39E43625D9F ON custom_forms (updated_at)'); - $this->addSql('CREATE TABLE documents (id INT NOT NULL, raw_document INT DEFAULT NULL, original INT DEFAULT NULL, raw BOOLEAN DEFAULT \'false\' NOT NULL, embedId VARCHAR(255) DEFAULT NULL, embedPlatform VARCHAR(255) DEFAULT NULL, filename VARCHAR(255) DEFAULT NULL, mime_type VARCHAR(255) DEFAULT NULL, folder VARCHAR(255) NOT NULL, private BOOLEAN DEFAULT \'false\' NOT NULL, imageWidth INT DEFAULT 0 NOT NULL, imageHeight INT DEFAULT 0 NOT NULL, average_color VARCHAR(7) DEFAULT NULL, filesize INT DEFAULT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_A2B0728826CBD5A5 ON documents (raw_document)'); - $this->addSql('CREATE INDEX IDX_A2B072882F727085 ON documents (original)'); - $this->addSql('CREATE INDEX IDX_A2B072881AB3DB55 ON documents (raw)'); - $this->addSql('CREATE INDEX IDX_A2B07288D206C1D1 ON documents (private)'); - $this->addSql('CREATE INDEX IDX_A2B072881AB3DB55D206C1D1 ON documents (raw, private)'); - $this->addSql('CREATE INDEX IDX_A2B072882100AA2E ON documents (mime_type)'); - $this->addSql('CREATE TABLE documents_translations (id INT NOT NULL, translation_id INT DEFAULT NULL, document_id INT DEFAULT NULL, name VARCHAR(255) DEFAULT NULL, description TEXT DEFAULT NULL, copyright TEXT DEFAULT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE INDEX IDX_5CD2F5509CAA2B25 ON documents_translations (translation_id)'); - $this->addSql('CREATE INDEX IDX_5CD2F550C33F7837 ON documents_translations (document_id)'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_5CD2F550C33F78379CAA2B25 ON documents_translations (document_id, translation_id)'); - $this->addSql('CREATE TABLE folders (id INT NOT NULL, parent_id INT DEFAULT NULL, folder_name VARCHAR(255) NOT NULL, visible BOOLEAN DEFAULT \'true\' NOT NULL, position DOUBLE PRECISION NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_FE37D30F47BC5813 ON folders (folder_name)'); - $this->addSql('CREATE INDEX IDX_FE37D30F727ACA70 ON folders (parent_id)'); - $this->addSql('CREATE INDEX IDX_FE37D30F7AB0E859 ON folders (visible)'); - $this->addSql('CREATE INDEX IDX_FE37D30F462CE4F5 ON folders (position)'); - $this->addSql('CREATE INDEX IDX_FE37D30F8B8E8428 ON folders (created_at)'); - $this->addSql('CREATE INDEX IDX_FE37D30F43625D9F ON folders (updated_at)'); - $this->addSql('CREATE TABLE documents_folders (folder_id INT NOT NULL, document_id INT NOT NULL, PRIMARY KEY(folder_id, document_id))'); - $this->addSql('CREATE INDEX IDX_617BB29C162CB942 ON documents_folders (folder_id)'); - $this->addSql('CREATE INDEX IDX_617BB29CC33F7837 ON documents_folders (document_id)'); - $this->addSql('CREATE TABLE folders_translations (id INT NOT NULL, folder_id INT DEFAULT NULL, translation_id INT DEFAULT NULL, name VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE INDEX IDX_9F6A68B2162CB942 ON folders_translations (folder_id)'); - $this->addSql('CREATE INDEX IDX_9F6A68B29CAA2B25 ON folders_translations (translation_id)'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_9F6A68B2162CB9429CAA2B25 ON folders_translations (folder_id, translation_id)'); - $this->addSql('CREATE TABLE log (id INT NOT NULL, user_id INT DEFAULT NULL, node_source_id INT DEFAULT NULL, username VARCHAR(255) DEFAULT NULL, message TEXT NOT NULL, level INT NOT NULL, datetime TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, client_ip VARCHAR(255) DEFAULT NULL, channel VARCHAR(255) DEFAULT NULL, additional_data JSON DEFAULT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE INDEX IDX_8F3F68C5A76ED395 ON log (user_id)'); - $this->addSql('CREATE INDEX IDX_8F3F68C58E831402 ON log (node_source_id)'); - $this->addSql('CREATE INDEX IDX_8F3F68C593F3C6CA ON log (datetime)'); - $this->addSql('CREATE INDEX IDX_8F3F68C59AEACC13 ON log (level)'); - $this->addSql('CREATE INDEX IDX_8F3F68C5F85E0677 ON log (username)'); - $this->addSql('CREATE INDEX IDX_8F3F68C5A2F98E47 ON log (channel)'); - $this->addSql('CREATE TABLE login_attempts (id INT NOT NULL, ip_address VARCHAR(50) DEFAULT NULL, date TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, blocks_login_until TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, username VARCHAR(255) NOT NULL, attempt_count INT DEFAULT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE INDEX IDX_9163C7FBF85E0677 ON login_attempts (username)'); - $this->addSql('CREATE INDEX IDX_9163C7FBEFF8A4EEF85E0677 ON login_attempts (blocks_login_until, username)'); - $this->addSql('CREATE INDEX IDX_9163C7FBEFF8A4EEF85E067722FFD58C ON login_attempts (blocks_login_until, username, ip_address)'); - $this->addSql('COMMENT ON COLUMN login_attempts.date IS \'(DC2Type:datetime_immutable)\''); - $this->addSql('CREATE TABLE node_type_fields (id INT NOT NULL, node_type_id INT DEFAULT NULL, name VARCHAR(255) NOT NULL, label VARCHAR(255) NOT NULL, placeholder VARCHAR(255) DEFAULT NULL, description TEXT DEFAULT NULL, default_values TEXT DEFAULT NULL, type INT NOT NULL, expanded BOOLEAN DEFAULT \'false\' NOT NULL, universal BOOLEAN DEFAULT \'false\' NOT NULL, exclude_from_search BOOLEAN DEFAULT \'false\' NOT NULL, min_length INT DEFAULT NULL, max_length INT DEFAULT NULL, indexed BOOLEAN DEFAULT \'false\' NOT NULL, visible BOOLEAN DEFAULT \'true\' NOT NULL, group_name VARCHAR(255) DEFAULT NULL, group_name_canonical VARCHAR(255) DEFAULT NULL, position DOUBLE PRECISION NOT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE INDEX IDX_1D3923596344C9E1 ON node_type_fields (node_type_id)'); - $this->addSql('CREATE INDEX IDX_1D3923597AB0E859 ON node_type_fields (visible)'); - $this->addSql('CREATE INDEX IDX_1D392359D9416D95 ON node_type_fields (indexed)'); - $this->addSql('CREATE INDEX IDX_1D392359462CE4F5 ON node_type_fields (position)'); - $this->addSql('CREATE INDEX IDX_1D39235977792576 ON node_type_fields (group_name)'); - $this->addSql('CREATE INDEX IDX_1D3923594BAF07A4 ON node_type_fields (group_name_canonical)'); - $this->addSql('CREATE INDEX IDX_1D3923598CDE5729 ON node_type_fields (type)'); - $this->addSql('CREATE INDEX IDX_1D392359A4B8F6E1 ON node_type_fields (universal)'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_1D3923595E237E066344C9E1 ON node_type_fields (name, node_type_id)'); - $this->addSql('CREATE TABLE node_types (id INT NOT NULL, name VARCHAR(255) NOT NULL, display_name VARCHAR(255) NOT NULL, description TEXT DEFAULT NULL, visible BOOLEAN DEFAULT \'true\' NOT NULL, publishable BOOLEAN DEFAULT \'false\' NOT NULL, reachable BOOLEAN DEFAULT \'true\' NOT NULL, hiding_nodes BOOLEAN DEFAULT \'false\' NOT NULL, hiding_non_reachable_nodes BOOLEAN DEFAULT \'false\' NOT NULL, color VARCHAR(255) DEFAULT NULL, default_ttl INT DEFAULT 0 NOT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_409B1BCC5E237E06 ON node_types (name)'); - $this->addSql('CREATE INDEX IDX_409B1BCC7AB0E859 ON node_types (visible)'); - $this->addSql('CREATE INDEX IDX_409B1BCC7697C594 ON node_types (publishable)'); - $this->addSql('CREATE INDEX IDX_409B1BCCFB696FF0 ON node_types (hiding_nodes)'); - $this->addSql('CREATE INDEX IDX_409B1BCC5A3C14C7 ON node_types (hiding_non_reachable_nodes)'); - $this->addSql('CREATE INDEX IDX_409B1BCC96ED695F ON node_types (reachable)'); - $this->addSql('CREATE TABLE nodes (id INT NOT NULL, parent_node_id INT DEFAULT NULL, node_name VARCHAR(255) NOT NULL, dynamic_node_name BOOLEAN DEFAULT \'true\' NOT NULL, home BOOLEAN DEFAULT \'false\' NOT NULL, visible BOOLEAN DEFAULT \'true\' NOT NULL, status INT NOT NULL, ttl INT DEFAULT 0 NOT NULL, locked BOOLEAN DEFAULT \'false\' NOT NULL, priority NUMERIC(2, 1) NOT NULL, hide_children BOOLEAN DEFAULT \'false\' NOT NULL, sterile BOOLEAN DEFAULT \'false\' NOT NULL, children_order VARCHAR(255) NOT NULL, children_order_direction VARCHAR(4) NOT NULL, position DOUBLE PRECISION NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, nodeType_id INT DEFAULT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_1D3D05FC9987F390 ON nodes (node_name)'); - $this->addSql('CREATE INDEX IDX_1D3D05FC47D04729 ON nodes (nodeType_id)'); - $this->addSql('CREATE INDEX IDX_1D3D05FC3445EB91 ON nodes (parent_node_id)'); - $this->addSql('CREATE INDEX IDX_1D3D05FC7AB0E859 ON nodes (visible)'); - $this->addSql('CREATE INDEX IDX_1D3D05FC7B00651C ON nodes (status)'); - $this->addSql('CREATE INDEX IDX_1D3D05FCEAD2C891 ON nodes (locked)'); - $this->addSql('CREATE INDEX IDX_1D3D05FCF32D8BE6 ON nodes (sterile)'); - $this->addSql('CREATE INDEX IDX_1D3D05FC462CE4F5 ON nodes (position)'); - $this->addSql('CREATE INDEX IDX_1D3D05FC8B8E8428 ON nodes (created_at)'); - $this->addSql('CREATE INDEX IDX_1D3D05FC43625D9F ON nodes (updated_at)'); - $this->addSql('CREATE INDEX IDX_1D3D05FC50E2D3D2 ON nodes (hide_children)'); - $this->addSql('CREATE INDEX IDX_1D3D05FC9987F3907B00651C ON nodes (node_name, status)'); - $this->addSql('CREATE INDEX IDX_1D3D05FC7AB0E8597B00651C ON nodes (visible, status)'); - $this->addSql('CREATE INDEX IDX_1D3D05FC7AB0E8597B00651C3445EB91 ON nodes (visible, status, parent_node_id)'); - $this->addSql('CREATE INDEX IDX_1D3D05FC7AB0E8593445EB91 ON nodes (visible, parent_node_id)'); - $this->addSql('CREATE INDEX IDX_1D3D05FC71D60CD0 ON nodes (home)'); - $this->addSql('CREATE TABLE nodes_tags (node_id INT NOT NULL, tag_id INT NOT NULL, PRIMARY KEY(node_id, tag_id))'); - $this->addSql('CREATE INDEX IDX_5B5CB38C460D9FD7 ON nodes_tags (node_id)'); - $this->addSql('CREATE INDEX IDX_5B5CB38CBAD26311 ON nodes_tags (tag_id)'); - $this->addSql('CREATE TABLE stack_types (node_id INT NOT NULL, nodetype_id INT NOT NULL, PRIMARY KEY(node_id, nodetype_id))'); - $this->addSql('CREATE INDEX IDX_DE24E53460D9FD7 ON stack_types (node_id)'); - $this->addSql('CREATE INDEX IDX_DE24E53886D7EB5 ON stack_types (nodetype_id)'); - $this->addSql('CREATE TABLE nodes_custom_forms (id INT NOT NULL, node_id INT DEFAULT NULL, custom_form_id INT DEFAULT NULL, node_type_field_id INT DEFAULT NULL, position DOUBLE PRECISION NOT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE INDEX IDX_4D401A0C460D9FD7 ON nodes_custom_forms (node_id)'); - $this->addSql('CREATE INDEX IDX_4D401A0C58AFF2B0 ON nodes_custom_forms (custom_form_id)'); - $this->addSql('CREATE INDEX IDX_4D401A0C47705282 ON nodes_custom_forms (node_type_field_id)'); - $this->addSql('CREATE INDEX IDX_4D401A0C462CE4F5 ON nodes_custom_forms (position)'); - $this->addSql('CREATE TABLE nodes_sources (id INT NOT NULL, node_id INT DEFAULT NULL, translation_id INT DEFAULT NULL, title VARCHAR(255) DEFAULT NULL, published_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, meta_title VARCHAR(255) NOT NULL, meta_keywords TEXT NOT NULL, meta_description TEXT NOT NULL, discr VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE INDEX IDX_7C7DED6D460D9FD7 ON nodes_sources (node_id)'); - $this->addSql('CREATE INDEX IDX_7C7DED6D9CAA2B25 ON nodes_sources (translation_id)'); - $this->addSql('CREATE INDEX IDX_7C7DED6D4AD26064 ON nodes_sources (discr)'); - $this->addSql('CREATE INDEX IDX_7C7DED6D4AD260649CAA2B25 ON nodes_sources (discr, translation_id)'); - $this->addSql('CREATE INDEX IDX_7C7DED6DE0D4FDE14AD260649CAA2B25 ON nodes_sources (published_at, discr, translation_id)'); - $this->addSql('CREATE INDEX IDX_7C7DED6D2B36786B ON nodes_sources (title)'); - $this->addSql('CREATE INDEX IDX_7C7DED6DE0D4FDE1 ON nodes_sources (published_at)'); - $this->addSql('CREATE INDEX IDX_7C7DED6DE0D4FDE19CAA2B25 ON nodes_sources (published_at, translation_id)'); - $this->addSql('CREATE INDEX IDX_7C7DED6D460D9FD79CAA2B25E0D4FDE1 ON nodes_sources (node_id, translation_id, published_at)'); - $this->addSql('CREATE INDEX IDX_7C7DED6D2B36786BE0D4FDE1 ON nodes_sources (title, published_at)'); - $this->addSql('CREATE INDEX IDX_7C7DED6D2B36786BE0D4FDE19CAA2B25 ON nodes_sources (title, published_at, translation_id)'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_7C7DED6D460D9FD79CAA2B25 ON nodes_sources (node_id, translation_id)'); - $this->addSql('CREATE TABLE nodes_sources_documents (id INT NOT NULL, ns_id INT DEFAULT NULL, document_id INT DEFAULT NULL, node_type_field_id INT DEFAULT NULL, position DOUBLE PRECISION NOT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE INDEX IDX_1CD104F7AA2D61 ON nodes_sources_documents (ns_id)'); - $this->addSql('CREATE INDEX IDX_1CD104F7C33F7837 ON nodes_sources_documents (document_id)'); - $this->addSql('CREATE INDEX IDX_1CD104F747705282 ON nodes_sources_documents (node_type_field_id)'); - $this->addSql('CREATE INDEX IDX_1CD104F7462CE4F5 ON nodes_sources_documents (position)'); - $this->addSql('CREATE INDEX IDX_1CD104F7AA2D6147705282 ON nodes_sources_documents (ns_id, node_type_field_id)'); - $this->addSql('CREATE INDEX IDX_1CD104F7AA2D6147705282462CE4F5 ON nodes_sources_documents (ns_id, node_type_field_id, position)'); - $this->addSql('CREATE TABLE nodes_to_nodes (id INT NOT NULL, node_a_id INT DEFAULT NULL, node_b_id INT DEFAULT NULL, node_type_field_id INT DEFAULT NULL, position DOUBLE PRECISION NOT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE INDEX IDX_761F9A91FC7ADECE ON nodes_to_nodes (node_a_id)'); - $this->addSql('CREATE INDEX IDX_761F9A91EECF7120 ON nodes_to_nodes (node_b_id)'); - $this->addSql('CREATE INDEX IDX_761F9A9147705282 ON nodes_to_nodes (node_type_field_id)'); - $this->addSql('CREATE INDEX IDX_761F9A91462CE4F5 ON nodes_to_nodes (position)'); - $this->addSql('CREATE INDEX IDX_761F9A91FC7ADECE47705282 ON nodes_to_nodes (node_a_id, node_type_field_id)'); - $this->addSql('CREATE INDEX IDX_761F9A91FC7ADECE47705282462CE4F5 ON nodes_to_nodes (node_a_id, node_type_field_id, position)'); - $this->addSql('CREATE INDEX IDX_761F9A91EECF712047705282 ON nodes_to_nodes (node_b_id, node_type_field_id)'); - $this->addSql('CREATE INDEX IDX_761F9A91EECF712047705282462CE4F5 ON nodes_to_nodes (node_b_id, node_type_field_id, position)'); - $this->addSql('CREATE TABLE redirections (id INT NOT NULL, ns_id INT DEFAULT NULL, query VARCHAR(255) NOT NULL, redirectUri VARCHAR(255) DEFAULT NULL, type INT NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_38F5ECE424BDB5EB ON redirections (query)'); - $this->addSql('CREATE INDEX IDX_38F5ECE4AA2D61 ON redirections (ns_id)'); - $this->addSql('CREATE INDEX IDX_38F5ECE48B8E8428 ON redirections (created_at)'); - $this->addSql('CREATE INDEX IDX_38F5ECE443625D9F ON redirections (updated_at)'); - $this->addSql('CREATE TABLE roles (id INT NOT NULL, name VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_B63E2EC75E237E06 ON roles (name)'); - $this->addSql('CREATE TABLE settings (id INT NOT NULL, setting_group_id INT DEFAULT NULL, name VARCHAR(255) NOT NULL, description TEXT DEFAULT NULL, value TEXT DEFAULT NULL, visible BOOLEAN DEFAULT \'true\' NOT NULL, encrypted BOOLEAN DEFAULT \'false\' NOT NULL, type INT NOT NULL, defaultValues TEXT DEFAULT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_E545A0C55E237E06 ON settings (name)'); - $this->addSql('CREATE INDEX IDX_E545A0C550DDE1BD ON settings (setting_group_id)'); - $this->addSql('CREATE INDEX IDX_E545A0C58CDE5729 ON settings (type)'); - $this->addSql('CREATE INDEX IDX_E545A0C55E237E06 ON settings (name)'); - $this->addSql('CREATE INDEX IDX_E545A0C57AB0E859 ON settings (visible)'); - $this->addSql('CREATE TABLE settings_groups (id INT NOT NULL, name VARCHAR(255) NOT NULL, in_menu BOOLEAN DEFAULT \'false\' NOT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_FFD519025E237E06 ON settings_groups (name)'); - $this->addSql('CREATE TABLE tags (id INT NOT NULL, parent_tag_id INT DEFAULT NULL, color VARCHAR(7) DEFAULT \'#000000\' NOT NULL, tag_name VARCHAR(255) NOT NULL, visible BOOLEAN DEFAULT \'true\' NOT NULL, children_order VARCHAR(255) DEFAULT \'position\' NOT NULL, children_order_direction VARCHAR(4) DEFAULT \'ASC\' NOT NULL, locked BOOLEAN DEFAULT \'false\' NOT NULL, position DOUBLE PRECISION NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_6FBC9426B02CC1B0 ON tags (tag_name)'); - $this->addSql('CREATE INDEX IDX_6FBC9426F5C1A0D7 ON tags (parent_tag_id)'); - $this->addSql('CREATE INDEX IDX_6FBC94267AB0E859 ON tags (visible)'); - $this->addSql('CREATE INDEX IDX_6FBC9426EAD2C891 ON tags (locked)'); - $this->addSql('CREATE INDEX IDX_6FBC9426462CE4F5 ON tags (position)'); - $this->addSql('CREATE INDEX IDX_6FBC94268B8E8428 ON tags (created_at)'); - $this->addSql('CREATE INDEX IDX_6FBC942643625D9F ON tags (updated_at)'); - $this->addSql('CREATE INDEX IDX_6FBC9426F5C1A0D77AB0E859 ON tags (parent_tag_id, visible)'); - $this->addSql('CREATE TABLE tags_translations (id INT NOT NULL, tag_id INT DEFAULT NULL, translation_id INT DEFAULT NULL, name VARCHAR(255) NOT NULL, description TEXT DEFAULT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE INDEX IDX_95D326DCBAD26311 ON tags_translations (tag_id)'); - $this->addSql('CREATE INDEX IDX_95D326DC9CAA2B25 ON tags_translations (translation_id)'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_95D326DCBAD263119CAA2B25 ON tags_translations (tag_id, translation_id)'); - $this->addSql('CREATE TABLE tags_translations_documents (id INT NOT NULL, tag_translation_id INT DEFAULT NULL, document_id INT DEFAULT NULL, position DOUBLE PRECISION NOT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE INDEX IDX_6E886F1F22010F1 ON tags_translations_documents (tag_translation_id)'); - $this->addSql('CREATE INDEX IDX_6E886F1FC33F7837 ON tags_translations_documents (document_id)'); - $this->addSql('CREATE INDEX IDX_6E886F1F462CE4F5 ON tags_translations_documents (position)'); - $this->addSql('CREATE INDEX IDX_6E886F1F22010F1462CE4F5 ON tags_translations_documents (tag_translation_id, position)'); - $this->addSql('CREATE TABLE translations (id INT NOT NULL, locale VARCHAR(10) NOT NULL, override_locale VARCHAR(10) DEFAULT NULL, name VARCHAR(255) NOT NULL, default_translation BOOLEAN DEFAULT \'false\' NOT NULL, available BOOLEAN DEFAULT \'true\' NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_C6B7DA874180C698 ON translations (locale)'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_C6B7DA873F824FD6 ON translations (override_locale)'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_C6B7DA875E237E06 ON translations (name)'); - $this->addSql('CREATE INDEX IDX_C6B7DA87A58FA485 ON translations (available)'); - $this->addSql('CREATE INDEX IDX_C6B7DA87609A56D9 ON translations (default_translation)'); - $this->addSql('CREATE INDEX IDX_C6B7DA878B8E8428 ON translations (created_at)'); - $this->addSql('CREATE INDEX IDX_C6B7DA8743625D9F ON translations (updated_at)'); - $this->addSql('CREATE INDEX IDX_C6B7DA87A58FA485609A56D9 ON translations (available, default_translation)'); - $this->addSql('CREATE INDEX IDX_C6B7DA87A58FA4854180C698 ON translations (available, locale)'); - $this->addSql('CREATE INDEX IDX_C6B7DA87A58FA4853F824FD6 ON translations (available, override_locale)'); - $this->addSql('CREATE TABLE url_aliases (id INT NOT NULL, ns_id INT DEFAULT NULL, alias VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_E261ED65E16C6B94 ON url_aliases (alias)'); - $this->addSql('CREATE INDEX IDX_E261ED65AA2D61 ON url_aliases (ns_id)'); - $this->addSql('CREATE TABLE user_log_entries (id INT NOT NULL, user_id INT DEFAULT NULL, action VARCHAR(8) NOT NULL, logged_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, object_id VARCHAR(64) DEFAULT NULL, object_class VARCHAR(191) NOT NULL, version INT NOT NULL, data TEXT DEFAULT NULL, username VARCHAR(191) DEFAULT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE INDEX IDX_BC2E42C7A76ED395 ON user_log_entries (user_id)'); - $this->addSql('CREATE INDEX log_class_lookup_idx ON user_log_entries (object_class)'); - $this->addSql('CREATE INDEX log_date_lookup_idx ON user_log_entries (logged_at)'); - $this->addSql('CREATE INDEX log_user_lookup_idx ON user_log_entries (username)'); - $this->addSql('CREATE INDEX log_version_lookup_idx ON user_log_entries (object_id, object_class, version)'); - $this->addSql('COMMENT ON COLUMN user_log_entries.data IS \'(DC2Type:array)\''); - $this->addSql('CREATE TABLE usergroups (id INT NOT NULL, name VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_98972EB45E237E06 ON usergroups (name)'); - $this->addSql('CREATE TABLE groups_roles (group_id INT NOT NULL, role_id INT NOT NULL, PRIMARY KEY(group_id, role_id))'); - $this->addSql('CREATE INDEX IDX_E79D4963FE54D947 ON groups_roles (group_id)'); - $this->addSql('CREATE INDEX IDX_E79D4963D60322AC ON groups_roles (role_id)'); - $this->addSql('CREATE TABLE users (id INT NOT NULL, chroot_id INT DEFAULT NULL, facebook_name VARCHAR(255) DEFAULT NULL, picture_url TEXT DEFAULT NULL, enabled BOOLEAN DEFAULT \'true\' NOT NULL, confirmation_token VARCHAR(255) DEFAULT NULL, password_requested_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, username VARCHAR(255) NOT NULL, salt VARCHAR(255) NOT NULL, password VARCHAR(255) NOT NULL, last_login TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, expired BOOLEAN DEFAULT \'false\' NOT NULL, locked BOOLEAN DEFAULT \'false\' NOT NULL, credentials_expires_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, credentials_expired BOOLEAN DEFAULT \'false\' NOT NULL, expires_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, locale VARCHAR(7) DEFAULT NULL, email VARCHAR(255) NOT NULL, firstName VARCHAR(255) DEFAULT NULL, lastName VARCHAR(255) DEFAULT NULL, phone VARCHAR(255) DEFAULT NULL, company VARCHAR(255) DEFAULT NULL, job VARCHAR(255) DEFAULT NULL, birthday TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_1483A5E9C05FB297 ON users (confirmation_token)'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_1483A5E9F85E0677 ON users (username)'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_1483A5E9E7927C74 ON users (email)'); - $this->addSql('CREATE INDEX IDX_1483A5E96483A539 ON users (chroot_id)'); - $this->addSql('CREATE INDEX IDX_1483A5E950F9BB84 ON users (enabled)'); - $this->addSql('CREATE INDEX IDX_1483A5E9194FED4B ON users (expired)'); - $this->addSql('CREATE INDEX IDX_1483A5E9F9D83E2 ON users (expires_at)'); - $this->addSql('CREATE INDEX IDX_1483A5E94180C698 ON users (locale)'); - $this->addSql('CREATE TABLE users_roles (user_id INT NOT NULL, role_id INT NOT NULL, PRIMARY KEY(user_id, role_id))'); - $this->addSql('CREATE INDEX IDX_51498A8EA76ED395 ON users_roles (user_id)'); - $this->addSql('CREATE INDEX IDX_51498A8ED60322AC ON users_roles (role_id)'); - $this->addSql('CREATE TABLE users_groups (user_id INT NOT NULL, group_id INT NOT NULL, PRIMARY KEY(user_id, group_id))'); - $this->addSql('CREATE INDEX IDX_FF8AB7E0A76ED395 ON users_groups (user_id)'); - $this->addSql('CREATE INDEX IDX_FF8AB7E0FE54D947 ON users_groups (group_id)'); - $this->addSql('ALTER TABLE attribute_group_translations ADD CONSTRAINT FK_5C704A6862D643B7 FOREIGN KEY (attribute_group_id) REFERENCES attribute_groups (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE attribute_group_translations ADD CONSTRAINT FK_5C704A689CAA2B25 FOREIGN KEY (translation_id) REFERENCES translations (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE attribute_translations ADD CONSTRAINT FK_4059D4A0B6E62EFA FOREIGN KEY (attribute_id) REFERENCES attributes (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE attribute_translations ADD CONSTRAINT FK_4059D4A09CAA2B25 FOREIGN KEY (translation_id) REFERENCES translations (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE attribute_value_translations ADD CONSTRAINT FK_1293849B9CAA2B25 FOREIGN KEY (translation_id) REFERENCES translations (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE attribute_value_translations ADD CONSTRAINT FK_1293849BFE4FBB82 FOREIGN KEY (attribute_value) REFERENCES attribute_values (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE attribute_values ADD CONSTRAINT FK_184662BCB6E62EFA FOREIGN KEY (attribute_id) REFERENCES attributes (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE attribute_values ADD CONSTRAINT FK_184662BC460D9FD7 FOREIGN KEY (node_id) REFERENCES nodes (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE attributes ADD CONSTRAINT FK_319B9E70FE54D947 FOREIGN KEY (group_id) REFERENCES attribute_groups (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE attributes_documents ADD CONSTRAINT FK_67CCC9E0B6E62EFA FOREIGN KEY (attribute_id) REFERENCES attributes (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE attributes_documents ADD CONSTRAINT FK_67CCC9E0C33F7837 FOREIGN KEY (document_id) REFERENCES documents (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE custom_form_answers ADD CONSTRAINT FK_1A3BB12658AFF2B0 FOREIGN KEY (custom_form_id) REFERENCES custom_forms (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE custom_form_field_attributes ADD CONSTRAINT FK_B7133605F1D6C2D1 FOREIGN KEY (custom_form_answer_id) REFERENCES custom_form_answers (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE custom_form_field_attributes ADD CONSTRAINT FK_B71336057F13CC0F FOREIGN KEY (custom_form_field_id) REFERENCES custom_form_fields (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE custom_form_answers_documents ADD CONSTRAINT FK_E979F877C84CA2FC FOREIGN KEY (customformfieldattribute_id) REFERENCES custom_form_field_attributes (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE custom_form_answers_documents ADD CONSTRAINT FK_E979F877C33F7837 FOREIGN KEY (document_id) REFERENCES documents (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE custom_form_fields ADD CONSTRAINT FK_4A3782EC58AFF2B0 FOREIGN KEY (custom_form_id) REFERENCES custom_forms (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE documents ADD CONSTRAINT FK_A2B0728826CBD5A5 FOREIGN KEY (raw_document) REFERENCES documents (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE documents ADD CONSTRAINT FK_A2B072882F727085 FOREIGN KEY (original) REFERENCES documents (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE documents_translations ADD CONSTRAINT FK_5CD2F5509CAA2B25 FOREIGN KEY (translation_id) REFERENCES translations (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE documents_translations ADD CONSTRAINT FK_5CD2F550C33F7837 FOREIGN KEY (document_id) REFERENCES documents (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE folders ADD CONSTRAINT FK_FE37D30F727ACA70 FOREIGN KEY (parent_id) REFERENCES folders (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE documents_folders ADD CONSTRAINT FK_617BB29C162CB942 FOREIGN KEY (folder_id) REFERENCES folders (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE documents_folders ADD CONSTRAINT FK_617BB29CC33F7837 FOREIGN KEY (document_id) REFERENCES documents (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE folders_translations ADD CONSTRAINT FK_9F6A68B2162CB942 FOREIGN KEY (folder_id) REFERENCES folders (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE folders_translations ADD CONSTRAINT FK_9F6A68B29CAA2B25 FOREIGN KEY (translation_id) REFERENCES translations (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE log ADD CONSTRAINT FK_8F3F68C5A76ED395 FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE log ADD CONSTRAINT FK_8F3F68C58E831402 FOREIGN KEY (node_source_id) REFERENCES nodes_sources (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE node_type_fields ADD CONSTRAINT FK_1D3923596344C9E1 FOREIGN KEY (node_type_id) REFERENCES node_types (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE nodes ADD CONSTRAINT FK_1D3D05FC47D04729 FOREIGN KEY (nodeType_id) REFERENCES node_types (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE nodes ADD CONSTRAINT FK_1D3D05FC3445EB91 FOREIGN KEY (parent_node_id) REFERENCES nodes (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE nodes_tags ADD CONSTRAINT FK_5B5CB38C460D9FD7 FOREIGN KEY (node_id) REFERENCES nodes (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE nodes_tags ADD CONSTRAINT FK_5B5CB38CBAD26311 FOREIGN KEY (tag_id) REFERENCES tags (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE stack_types ADD CONSTRAINT FK_DE24E53460D9FD7 FOREIGN KEY (node_id) REFERENCES nodes (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE stack_types ADD CONSTRAINT FK_DE24E53886D7EB5 FOREIGN KEY (nodetype_id) REFERENCES node_types (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE nodes_custom_forms ADD CONSTRAINT FK_4D401A0C460D9FD7 FOREIGN KEY (node_id) REFERENCES nodes (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE nodes_custom_forms ADD CONSTRAINT FK_4D401A0C58AFF2B0 FOREIGN KEY (custom_form_id) REFERENCES custom_forms (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE nodes_custom_forms ADD CONSTRAINT FK_4D401A0C47705282 FOREIGN KEY (node_type_field_id) REFERENCES node_type_fields (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE nodes_sources ADD CONSTRAINT FK_7C7DED6D460D9FD7 FOREIGN KEY (node_id) REFERENCES nodes (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE nodes_sources ADD CONSTRAINT FK_7C7DED6D9CAA2B25 FOREIGN KEY (translation_id) REFERENCES translations (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE nodes_sources_documents ADD CONSTRAINT FK_1CD104F7AA2D61 FOREIGN KEY (ns_id) REFERENCES nodes_sources (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE nodes_sources_documents ADD CONSTRAINT FK_1CD104F7C33F7837 FOREIGN KEY (document_id) REFERENCES documents (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE nodes_sources_documents ADD CONSTRAINT FK_1CD104F747705282 FOREIGN KEY (node_type_field_id) REFERENCES node_type_fields (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE nodes_to_nodes ADD CONSTRAINT FK_761F9A91FC7ADECE FOREIGN KEY (node_a_id) REFERENCES nodes (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE nodes_to_nodes ADD CONSTRAINT FK_761F9A91EECF7120 FOREIGN KEY (node_b_id) REFERENCES nodes (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE nodes_to_nodes ADD CONSTRAINT FK_761F9A9147705282 FOREIGN KEY (node_type_field_id) REFERENCES node_type_fields (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE redirections ADD CONSTRAINT FK_38F5ECE4AA2D61 FOREIGN KEY (ns_id) REFERENCES nodes_sources (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE settings ADD CONSTRAINT FK_E545A0C550DDE1BD FOREIGN KEY (setting_group_id) REFERENCES settings_groups (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE tags ADD CONSTRAINT FK_6FBC9426F5C1A0D7 FOREIGN KEY (parent_tag_id) REFERENCES tags (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE tags_translations ADD CONSTRAINT FK_95D326DCBAD26311 FOREIGN KEY (tag_id) REFERENCES tags (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE tags_translations ADD CONSTRAINT FK_95D326DC9CAA2B25 FOREIGN KEY (translation_id) REFERENCES translations (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE tags_translations_documents ADD CONSTRAINT FK_6E886F1F22010F1 FOREIGN KEY (tag_translation_id) REFERENCES tags_translations (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE tags_translations_documents ADD CONSTRAINT FK_6E886F1FC33F7837 FOREIGN KEY (document_id) REFERENCES documents (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE url_aliases ADD CONSTRAINT FK_E261ED65AA2D61 FOREIGN KEY (ns_id) REFERENCES nodes_sources (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE user_log_entries ADD CONSTRAINT FK_BC2E42C7A76ED395 FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE groups_roles ADD CONSTRAINT FK_E79D4963FE54D947 FOREIGN KEY (group_id) REFERENCES usergroups (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE groups_roles ADD CONSTRAINT FK_E79D4963D60322AC FOREIGN KEY (role_id) REFERENCES roles (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE users ADD CONSTRAINT FK_1483A5E96483A539 FOREIGN KEY (chroot_id) REFERENCES nodes (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE users_roles ADD CONSTRAINT FK_51498A8EA76ED395 FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE users_roles ADD CONSTRAINT FK_51498A8ED60322AC FOREIGN KEY (role_id) REFERENCES roles (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE users_groups ADD CONSTRAINT FK_FF8AB7E0A76ED395 FOREIGN KEY (user_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE users_groups ADD CONSTRAINT FK_FF8AB7E0FE54D947 FOREIGN KEY (group_id) REFERENCES usergroups (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->write('Nothing to do with RZ\Roadiz\Migrations\Version20201225181256.'); } public function down(Schema $schema) : void diff --git a/migrations/Version20230607134403.php b/migrations/Version20230607134403.php new file mode 100644 index 00000000..7b50e986 --- /dev/null +++ b/migrations/Version20230607134403.php @@ -0,0 +1,80 @@ +addSql('ALTER TABLE attribute_translations CHANGE label label VARCHAR(250) NOT NULL'); + $this->addSql('ALTER TABLE custom_form_answers CHANGE ip ip VARCHAR(46) NOT NULL'); + $this->addSql('ALTER TABLE custom_form_fields CHANGE name name VARCHAR(250) NOT NULL, CHANGE label label VARCHAR(250) NOT NULL, CHANGE placeholder placeholder VARCHAR(250) DEFAULT NULL, CHANGE group_name group_name VARCHAR(250) DEFAULT NULL, CHANGE group_name_canonical group_name_canonical VARCHAR(250) DEFAULT NULL'); + $this->addSql('ALTER TABLE custom_forms CHANGE color color VARCHAR(7) DEFAULT NULL, CHANGE name name VARCHAR(250) NOT NULL, CHANGE display_name display_name VARCHAR(250) NOT NULL'); + $this->addSql('ALTER TABLE documents CHANGE embedId embedId VARCHAR(250) DEFAULT NULL, CHANGE embedPlatform embedPlatform VARCHAR(100) DEFAULT NULL, CHANGE filename filename VARCHAR(250) DEFAULT NULL, CHANGE folder folder VARCHAR(100) NOT NULL'); + $this->addSql('ALTER TABLE documents_translations CHANGE name name VARCHAR(250) DEFAULT NULL'); + $this->addSql('ALTER TABLE folders CHANGE folder_name folder_name VARCHAR(250) NOT NULL'); + $this->addSql('ALTER TABLE folders_translations CHANGE name name VARCHAR(250) NOT NULL'); + $this->addSql('ALTER TABLE log CHANGE client_ip client_ip VARCHAR(46) DEFAULT NULL, CHANGE channel channel VARCHAR(64) DEFAULT NULL'); + $this->addSql('ALTER TABLE login_attempts CHANGE ip_address ip_address VARCHAR(46) DEFAULT NULL'); + $this->addSql('ALTER TABLE node_type_fields CHANGE name name VARCHAR(250) NOT NULL, CHANGE label label VARCHAR(250) NOT NULL, CHANGE placeholder placeholder VARCHAR(250) DEFAULT NULL, CHANGE group_name group_name VARCHAR(250) DEFAULT NULL, CHANGE group_name_canonical group_name_canonical VARCHAR(250) DEFAULT NULL'); + $this->addSql('ALTER TABLE node_types CHANGE name name VARCHAR(30) NOT NULL, CHANGE display_name display_name VARCHAR(250) NOT NULL, CHANGE color color VARCHAR(7) DEFAULT NULL'); + $this->addSql('ALTER TABLE nodes CHANGE children_order children_order VARCHAR(50) NOT NULL'); + $this->addSql('ALTER TABLE nodes_sources CHANGE title title VARCHAR(250) DEFAULT NULL, CHANGE meta_title meta_title VARCHAR(150) NOT NULL'); + $this->addSql('ALTER TABLE roles CHANGE name name VARCHAR(250) NOT NULL'); + $this->addSql('ALTER TABLE settings CHANGE name name VARCHAR(250) NOT NULL'); + $this->addSql('ALTER TABLE settings_groups CHANGE name name VARCHAR(250) NOT NULL'); + $this->addSql('ALTER TABLE tags CHANGE tag_name tag_name VARCHAR(250) NOT NULL, CHANGE children_order children_order VARCHAR(60) DEFAULT \'position\' NOT NULL'); + $this->addSql('ALTER TABLE tags_translations CHANGE name name VARCHAR(250) NOT NULL'); + $this->addSql('ALTER TABLE translations CHANGE name name VARCHAR(250) NOT NULL'); + $this->addSql('ALTER TABLE url_aliases CHANGE alias alias VARCHAR(250) NOT NULL'); + $this->addSql('ALTER TABLE usergroups CHANGE name name VARCHAR(250) NOT NULL'); + $this->addSql('ALTER TABLE users CHANGE facebook_name facebook_name VARCHAR(128) DEFAULT NULL, CHANGE confirmation_token confirmation_token VARCHAR(128) DEFAULT NULL, CHANGE username username VARCHAR(200) NOT NULL, CHANGE salt salt VARCHAR(64) NOT NULL, CHANGE password password VARCHAR(128) NOT NULL, CHANGE email email VARCHAR(200) NOT NULL, CHANGE firstName firstName VARCHAR(250) DEFAULT NULL, CHANGE lastName lastName VARCHAR(250) DEFAULT NULL, CHANGE phone phone VARCHAR(50) DEFAULT NULL, CHANGE company company VARCHAR(250) DEFAULT NULL, CHANGE job job VARCHAR(250) DEFAULT NULL, CHANGE publicName publicName VARCHAR(250) DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE attribute_translations CHANGE label label VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`'); + $this->addSql('ALTER TABLE custom_form_answers CHANGE ip ip VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`'); + $this->addSql('ALTER TABLE custom_form_fields CHANGE group_name group_name VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, CHANGE group_name_canonical group_name_canonical VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, CHANGE name name VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`, CHANGE label label VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`, CHANGE placeholder placeholder VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`'); + $this->addSql('ALTER TABLE custom_forms CHANGE color color VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, CHANGE name name VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`, CHANGE display_name display_name VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`'); + $this->addSql('ALTER TABLE documents CHANGE embedId embedId VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, CHANGE embedPlatform embedPlatform VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, CHANGE filename filename VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, CHANGE folder folder VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`'); + $this->addSql('ALTER TABLE documents_translations CHANGE name name VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`'); + $this->addSql('ALTER TABLE folders CHANGE folder_name folder_name VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`'); + $this->addSql('ALTER TABLE folders_translations CHANGE name name VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`'); + $this->addSql('ALTER TABLE log CHANGE client_ip client_ip VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, CHANGE channel channel VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`'); + $this->addSql('ALTER TABLE login_attempts CHANGE ip_address ip_address VARCHAR(50) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`'); + $this->addSql('ALTER TABLE node_type_fields CHANGE name name VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`, CHANGE group_name group_name VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, CHANGE group_name_canonical group_name_canonical VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, CHANGE label label VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`, CHANGE placeholder placeholder VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`'); + $this->addSql('ALTER TABLE node_types CHANGE color color VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, CHANGE name name VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`, CHANGE display_name display_name VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`'); + $this->addSql('ALTER TABLE nodes CHANGE children_order children_order VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`'); + $this->addSql('ALTER TABLE nodes_sources CHANGE title title VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, CHANGE meta_title meta_title VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`'); + $this->addSql('ALTER TABLE roles CHANGE name name VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`'); + $this->addSql('ALTER TABLE settings CHANGE name name VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`'); + $this->addSql('ALTER TABLE settings_groups CHANGE name name VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`'); + $this->addSql('ALTER TABLE tags CHANGE tag_name tag_name VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`, CHANGE children_order children_order VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT \'position\' NOT NULL COLLATE `utf8mb4_unicode_ci`'); + $this->addSql('ALTER TABLE tags_translations CHANGE name name VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`'); + $this->addSql('ALTER TABLE translations CHANGE name name VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`'); + $this->addSql('ALTER TABLE url_aliases CHANGE alias alias VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`'); + $this->addSql('ALTER TABLE usergroups CHANGE name name VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`'); + $this->addSql('ALTER TABLE users CHANGE email email VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`, CHANGE facebook_name facebook_name VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, CHANGE confirmation_token confirmation_token VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, CHANGE username username VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`, CHANGE salt salt VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`, CHANGE password password VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`, CHANGE publicName publicName VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, CHANGE firstName firstName VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, CHANGE lastName lastName VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, CHANGE phone phone VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, CHANGE company company VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, CHANGE job job VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`'); + } + + public function isTransactional(): bool + { + return false; + } +} diff --git a/migrations/Version20230615122615.php b/migrations/Version20230615122615.php new file mode 100644 index 00000000..abed8b48 --- /dev/null +++ b/migrations/Version20230615122615.php @@ -0,0 +1,38 @@ +addSql('ALTER TABLE redirections ADD use_count INT DEFAULT 0 NOT NULL'); + $this->addSql('CREATE INDEX redirection_use_count ON redirections (use_count)'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('DROP INDEX redirection_use_count ON redirections'); + $this->addSql('ALTER TABLE redirections DROP use_count'); + } + + public function isTransactional(): bool + { + return false; + } +} diff --git a/migrations/Version20230628143106.php b/migrations/Version20230628143106.php new file mode 100644 index 00000000..98f4c214 --- /dev/null +++ b/migrations/Version20230628143106.php @@ -0,0 +1,74 @@ +addSql('ALTER TABLE log ADD entity_class VARCHAR(255) DEFAULT NULL, ADD entity_id VARCHAR(36) DEFAULT NULL'); + $this->addSql('CREATE INDEX IDX_8F3F68C541BF2C66 ON log (entity_class)'); + $this->addSql('CREATE INDEX IDX_8F3F68C541BF2C6681257D5D ON log (entity_class, entity_id)'); + $this->addSql('CREATE INDEX log_entity_class_datetime ON log (entity_class, datetime)'); + $this->addSql('CREATE INDEX log_entity_class_id_datetime ON log (entity_class, entity_id, datetime)'); + + // Move node_source_id to entity_class and entity_id + $nodeSourceClass = NodesSources::class; + $this->addSql(<<addSql('ALTER TABLE log DROP FOREIGN KEY FK_8F3F68C58E831402'); + $this->addSql('ALTER TABLE log DROP node_source_id'); + $this->addSql('DROP INDEX log_ns_datetime ON log'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE log ADD node_source_id INT DEFAULT NULL'); + + // Move entity_class and entity_id to node_source_id + $nodeSourceClass = NodesSources::class; + $this->addSql(<<addSql('DROP INDEX IDX_8F3F68C541BF2C66 ON log'); + $this->addSql('DROP INDEX IDX_8F3F68C541BF2C6681257D5D ON log'); + $this->addSql('DROP INDEX log_entity_class_datetime ON log'); + $this->addSql('DROP INDEX log_entity_class_id_datetime ON log'); + $this->addSql('ALTER TABLE log DROP entity_class, DROP entity_id'); + $this->addSql('ALTER TABLE log ADD CONSTRAINT FK_8F3F68C58E831402 FOREIGN KEY (node_source_id) REFERENCES nodes_sources (id) ON UPDATE NO ACTION ON DELETE SET NULL'); + $this->addSql('CREATE INDEX log_ns_datetime ON log (node_source_id, datetime)'); + $this->addSql('CREATE INDEX IDX_8F3F68C58E831402 ON log (node_source_id)'); + } + + public function isTransactional(): bool + { + return false; + } +} diff --git a/migrations/Version20230628170203.php b/migrations/Version20230628170203.php new file mode 100644 index 00000000..84dc01b3 --- /dev/null +++ b/migrations/Version20230628170203.php @@ -0,0 +1,40 @@ +addSql('ALTER TABLE log DROP FOREIGN KEY FK_8F3F68C5A76ED395'); + $this->addSql('DROP INDEX IDX_8F3F68C5A76ED395 ON log'); + $this->addSql('ALTER TABLE log CHANGE user_id user_id VARCHAR(36) DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE log CHANGE user_id user_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE log ADD CONSTRAINT FK_8F3F68C5A76ED395 FOREIGN KEY (user_id) REFERENCES users (id) ON UPDATE NO ACTION ON DELETE SET NULL'); + $this->addSql('CREATE INDEX IDX_8F3F68C5A76ED395 ON log (user_id)'); + } + + public function isTransactional(): bool + { + return false; + } +} diff --git a/migrations/Version20230712171650.php b/migrations/Version20230712171650.php new file mode 100644 index 00000000..cf03d449 --- /dev/null +++ b/migrations/Version20230712171650.php @@ -0,0 +1,36 @@ +addSql('DROP TABLE login_attempts'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE TABLE login_attempts (id INT AUTO_INCREMENT NOT NULL, ip_address VARCHAR(46) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, date DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', blocks_login_until DATETIME DEFAULT NULL, username VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`, attempt_count INT DEFAULT NULL, INDEX IDX_9163C7FBEFF8A4EEF85E0677 (blocks_login_until, username), INDEX IDX_9163C7FBEFF8A4EEF85E067722FFD58C (blocks_login_until, username, ip_address), INDEX IDX_9163C7FBF85E0677 (username), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB COMMENT = \'\' '); + } + + public function isTransactional(): bool + { + return false; + } +} diff --git a/migrations/Version20230828092821.php b/migrations/Version20230828092821.php new file mode 100644 index 00000000..5eebad1f --- /dev/null +++ b/migrations/Version20230828092821.php @@ -0,0 +1,46 @@ +addSql('CREATE INDEX custom_form_created_at ON custom_forms (created_at)'); + $this->addSql('CREATE INDEX custom_form_updated_at ON custom_forms (updated_at)'); + $this->addSql('CREATE INDEX redirection_created_at ON redirections (created_at)'); + $this->addSql('CREATE INDEX redirection_updated_at ON redirections (updated_at)'); + $this->addSql('CREATE INDEX idx_user_created_at ON users (created_at)'); + $this->addSql('CREATE INDEX idx_user_updated_at ON users (updated_at)'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('DROP INDEX custom_form_created_at ON custom_forms'); + $this->addSql('DROP INDEX custom_form_updated_at ON custom_forms'); + $this->addSql('DROP INDEX redirection_created_at ON redirections'); + $this->addSql('DROP INDEX redirection_updated_at ON redirections'); + $this->addSql('DROP INDEX idx_user_created_at ON users'); + $this->addSql('DROP INDEX idx_user_updated_at ON users'); + } + + public function isTransactional(): bool + { + return false; + } +} diff --git a/migrations/Version20230829082257.php b/migrations/Version20230829082257.php new file mode 100644 index 00000000..fc6b3521 --- /dev/null +++ b/migrations/Version20230829082257.php @@ -0,0 +1,38 @@ +addSql('CREATE INDEX idx_attribute_value_node_position ON attribute_values (node_id, position)'); + $this->addSql('CREATE INDEX idx_attribute_value_position ON attribute_values (position)'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('DROP INDEX idx_attribute_value_node_position ON attribute_values'); + $this->addSql('DROP INDEX idx_attribute_value_position ON attribute_values'); + } + + public function isTransactional(): bool + { + return false; + } +} diff --git a/migrations/Version20230905140844.php b/migrations/Version20230905140844.php new file mode 100644 index 00000000..823029ea --- /dev/null +++ b/migrations/Version20230905140844.php @@ -0,0 +1,46 @@ +addSql('ALTER TABLE attribute_values ADD realm_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE attribute_values ADD CONSTRAINT FK_184662BC9DFF5F89 FOREIGN KEY (realm_id) REFERENCES realms (id) ON DELETE SET NULL'); + $this->addSql('CREATE INDEX IDX_184662BC9DFF5F89 ON attribute_values (realm_id)'); + $this->addSql('ALTER TABLE attributes ADD realm_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE attributes ADD CONSTRAINT FK_319B9E709DFF5F89 FOREIGN KEY (realm_id) REFERENCES realms (id) ON DELETE SET NULL'); + $this->addSql('CREATE INDEX IDX_319B9E709DFF5F89 ON attributes (realm_id)'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE attribute_values DROP FOREIGN KEY FK_184662BC9DFF5F89'); + $this->addSql('DROP INDEX IDX_184662BC9DFF5F89 ON attribute_values'); + $this->addSql('ALTER TABLE attribute_values DROP realm_id'); + $this->addSql('ALTER TABLE attributes DROP FOREIGN KEY FK_319B9E709DFF5F89'); + $this->addSql('DROP INDEX IDX_319B9E709DFF5F89 ON attributes'); + $this->addSql('ALTER TABLE attributes DROP realm_id'); + } + + public function isTransactional(): bool + { + return false; + } +} diff --git a/migrations/Version20230915134833.php b/migrations/Version20230915134833.php new file mode 100644 index 00000000..cc0bd784 --- /dev/null +++ b/migrations/Version20230915134833.php @@ -0,0 +1,36 @@ +addSql('ALTER TABLE users DROP salt'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE users ADD salt VARCHAR(64) NOT NULL'); + } + + public function isTransactional(): bool + { + return false; + } +} diff --git a/migrations/Version20231012154717.php b/migrations/Version20231012154717.php new file mode 100644 index 00000000..5b71054d --- /dev/null +++ b/migrations/Version20231012154717.php @@ -0,0 +1,36 @@ +addSql('ALTER TABLE documents ADD image_crop_alignment VARCHAR(12) DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE documents DROP image_crop_alignment'); + } + + public function isTransactional(): bool + { + return false; + } +} diff --git a/migrations/Version20231013132932.php b/migrations/Version20231013132932.php new file mode 100644 index 00000000..b4b0829a --- /dev/null +++ b/migrations/Version20231013132932.php @@ -0,0 +1,38 @@ +addSql('ALTER TABLE node_types ADD attributable TINYINT(1) DEFAULT 1 NOT NULL'); + $this->addSql('CREATE INDEX IDX_409B1BCC1F470BBD ON node_types (attributable)'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('DROP INDEX IDX_409B1BCC1F470BBD ON node_types'); + $this->addSql('ALTER TABLE node_types DROP attributable'); + } + + public function isTransactional(): bool + { + return false; + } +} diff --git a/migrations/Version20240214143213.php b/migrations/Version20240214143213.php new file mode 100644 index 00000000..c1fae99a --- /dev/null +++ b/migrations/Version20240214143213.php @@ -0,0 +1,31 @@ +addSql('ALTER TABLE nodes_sources DROP meta_keywords'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE nodes_sources ADD meta_keywords LONGTEXT NOT NULL'); + } +} diff --git a/migrations/Version20240214143849.php b/migrations/Version20240214143849.php new file mode 100644 index 00000000..677a88f2 --- /dev/null +++ b/migrations/Version20240214143849.php @@ -0,0 +1,31 @@ +addSql('ALTER TABLE nodes DROP priority'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE nodes ADD priority NUMERIC(2, 1) NOT NULL'); + } +} diff --git a/migrations/Version20240214145403.php b/migrations/Version20240214145403.php new file mode 100644 index 00000000..a20f4e63 --- /dev/null +++ b/migrations/Version20240214145403.php @@ -0,0 +1,31 @@ +addSql('ALTER TABLE settings DROP encrypted'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE settings ADD encrypted TINYINT(1) DEFAULT 0 NOT NULL'); + } +} diff --git a/migrations/Version20240305124809.php b/migrations/Version20240305124809.php new file mode 100644 index 00000000..8f7e1c46 --- /dev/null +++ b/migrations/Version20240305124809.php @@ -0,0 +1,31 @@ +addSql('ALTER TABLE realms_nodes CHANGE realm_id realm_id INT NOT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE realms_nodes CHANGE realm_id realm_id INT DEFAULT NULL'); + } +} diff --git a/migrations/Version20240305125653.php b/migrations/Version20240305125653.php new file mode 100644 index 00000000..254249fe --- /dev/null +++ b/migrations/Version20240305125653.php @@ -0,0 +1,31 @@ +addSql('ALTER TABLE nodes_sources_documents CHANGE ns_id ns_id INT NOT NULL, CHANGE document_id document_id INT NOT NULL, CHANGE node_type_field_id node_type_field_id INT NOT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE nodes_sources_documents CHANGE ns_id ns_id INT DEFAULT NULL, CHANGE document_id document_id INT DEFAULT NULL, CHANGE node_type_field_id node_type_field_id INT DEFAULT NULL'); + } +} diff --git a/migrations/Version20240305132609.php b/migrations/Version20240305132609.php new file mode 100644 index 00000000..13ccd8a8 --- /dev/null +++ b/migrations/Version20240305132609.php @@ -0,0 +1,31 @@ +addSql('ALTER TABLE nodes_to_nodes CHANGE node_a_id node_a_id INT NOT NULL, CHANGE node_b_id node_b_id INT NOT NULL, CHANGE node_type_field_id node_type_field_id INT NOT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE nodes_to_nodes CHANGE node_a_id node_a_id INT DEFAULT NULL, CHANGE node_b_id node_b_id INT DEFAULT NULL, CHANGE node_type_field_id node_type_field_id INT DEFAULT NULL'); + } +} diff --git a/migrations/Version20240305133122.php b/migrations/Version20240305133122.php new file mode 100644 index 00000000..f21faee5 --- /dev/null +++ b/migrations/Version20240305133122.php @@ -0,0 +1,31 @@ +addSql('ALTER TABLE nodes_custom_forms CHANGE node_id node_id INT NOT NULL, CHANGE custom_form_id custom_form_id INT NOT NULL, CHANGE node_type_field_id node_type_field_id INT NOT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE nodes_custom_forms CHANGE node_id node_id INT DEFAULT NULL, CHANGE custom_form_id custom_form_id INT DEFAULT NULL, CHANGE node_type_field_id node_type_field_id INT DEFAULT NULL'); + } +} diff --git a/migrations/Version20240305133641.php b/migrations/Version20240305133641.php new file mode 100644 index 00000000..ba74548a --- /dev/null +++ b/migrations/Version20240305133641.php @@ -0,0 +1,31 @@ +addSql('ALTER TABLE nodes_sources CHANGE node_id node_id INT NOT NULL, CHANGE translation_id translation_id INT NOT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE nodes_sources CHANGE node_id node_id INT DEFAULT NULL, CHANGE translation_id translation_id INT DEFAULT NULL'); + } +} diff --git a/migrations/Version20240305134734.php b/migrations/Version20240305134734.php new file mode 100644 index 00000000..0bb9c334 --- /dev/null +++ b/migrations/Version20240305134734.php @@ -0,0 +1,37 @@ +addSql('ALTER TABLE documents_translations CHANGE translation_id translation_id INT NOT NULL, CHANGE document_id document_id INT NOT NULL'); + $this->addSql('ALTER TABLE folders_translations CHANGE folder_id folder_id INT NOT NULL, CHANGE translation_id translation_id INT NOT NULL'); + $this->addSql('ALTER TABLE tags_translations CHANGE tag_id tag_id INT NOT NULL, CHANGE translation_id translation_id INT NOT NULL'); + $this->addSql('ALTER TABLE tags_translations_documents CHANGE tag_translation_id tag_translation_id INT NOT NULL, CHANGE document_id document_id INT NOT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE documents_translations CHANGE translation_id translation_id INT DEFAULT NULL, CHANGE document_id document_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE folders_translations CHANGE folder_id folder_id INT DEFAULT NULL, CHANGE translation_id translation_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE tags_translations CHANGE tag_id tag_id INT DEFAULT NULL, CHANGE translation_id translation_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE tags_translations_documents CHANGE tag_translation_id tag_translation_id INT DEFAULT NULL, CHANGE document_id document_id INT DEFAULT NULL'); + } +} diff --git a/migrations/Version20240305142243.php b/migrations/Version20240305142243.php new file mode 100644 index 00000000..9a8f36a5 --- /dev/null +++ b/migrations/Version20240305142243.php @@ -0,0 +1,62 @@ +addSql('ALTER TABLE attribute_group_translations CHANGE attribute_group_id attribute_group_id INT NOT NULL, CHANGE translation_id translation_id INT NOT NULL'); + $this->addSql('ALTER TABLE attribute_translations CHANGE attribute_id attribute_id INT NOT NULL, CHANGE translation_id translation_id INT NOT NULL'); + $this->addSql('ALTER TABLE attribute_value_translations CHANGE translation_id translation_id INT NOT NULL, CHANGE attribute_value attribute_value INT NOT NULL'); + $this->addSql('ALTER TABLE attribute_values CHANGE attribute_id attribute_id INT NOT NULL, CHANGE node_id node_id INT NOT NULL'); + $this->addSql('ALTER TABLE attributes_documents CHANGE attribute_id attribute_id INT NOT NULL, CHANGE document_id document_id INT NOT NULL'); + $this->addSql('ALTER TABLE custom_form_answers CHANGE custom_form_id custom_form_id INT NOT NULL'); + $this->addSql('ALTER TABLE custom_form_field_attributes CHANGE custom_form_answer_id custom_form_answer_id INT NOT NULL, CHANGE custom_form_field_id custom_form_field_id INT NOT NULL'); + $this->addSql('ALTER TABLE custom_form_fields CHANGE custom_form_id custom_form_id INT NOT NULL'); + + // Remove all node_type_fields where node_type_id is null before changing it to not-nullable + $this->addSql('DELETE FROM node_type_fields WHERE node_type_id IS NULL'); + $this->addSql('ALTER TABLE node_type_fields CHANGE node_type_id node_type_id INT NOT NULL'); + + // Remove all nodes where nodeType_id is null before changing it to not-nullable + $this->addSql('DELETE FROM nodes WHERE nodeType_id IS NULL'); + $this->addSql('ALTER TABLE nodes CHANGE nodeType_id nodeType_id INT NOT NULL'); + + // Remove all url_aliases where ns_id is null before changing it to not-nullable + $this->addSql('DELETE FROM url_aliases WHERE ns_id IS NULL'); + $this->addSql('ALTER TABLE url_aliases DROP FOREIGN KEY FK_E261ED65AA2D61'); + $this->addSql('ALTER TABLE url_aliases CHANGE ns_id ns_id INT NOT NULL'); + $this->addSql('ALTER TABLE url_aliases ADD CONSTRAINT FK_E261ED65AA2D61 FOREIGN KEY (ns_id) REFERENCES nodes_sources (id) ON DELETE CASCADE'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE attribute_group_translations CHANGE translation_id translation_id INT DEFAULT NULL, CHANGE attribute_group_id attribute_group_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE attribute_translations CHANGE translation_id translation_id INT DEFAULT NULL, CHANGE attribute_id attribute_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE attribute_value_translations CHANGE translation_id translation_id INT DEFAULT NULL, CHANGE attribute_value attribute_value INT DEFAULT NULL'); + $this->addSql('ALTER TABLE attribute_values CHANGE node_id node_id INT DEFAULT NULL, CHANGE attribute_id attribute_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE attributes_documents CHANGE attribute_id attribute_id INT DEFAULT NULL, CHANGE document_id document_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE custom_form_answers CHANGE custom_form_id custom_form_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE custom_form_field_attributes CHANGE custom_form_answer_id custom_form_answer_id INT DEFAULT NULL, CHANGE custom_form_field_id custom_form_field_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE custom_form_fields CHANGE custom_form_id custom_form_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE node_type_fields CHANGE node_type_id node_type_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE nodes CHANGE nodeType_id nodeType_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE url_aliases DROP FOREIGN KEY FK_E261ED65AA2D61'); + $this->addSql('ALTER TABLE url_aliases CHANGE ns_id ns_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE url_aliases ADD CONSTRAINT FK_E261ED65AA2D61 FOREIGN KEY (ns_id) REFERENCES nodes_sources (id) ON UPDATE NO ACTION ON DELETE NO ACTION'); + } +} diff --git a/migrations/Version20240305143443.php b/migrations/Version20240305143443.php new file mode 100644 index 00000000..f2469a84 --- /dev/null +++ b/migrations/Version20240305143443.php @@ -0,0 +1,31 @@ +addSql('CREATE INDEX node_parent_position ON nodes (parent_node_id, position)'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('DROP INDEX node_parent_position ON nodes'); + } +} diff --git a/migrations/Version20240318184555.php b/migrations/Version20240318184555.php new file mode 100644 index 00000000..e03fcf8c --- /dev/null +++ b/migrations/Version20240318184555.php @@ -0,0 +1,34 @@ +addSql('ALTER TABLE nodes_custom_forms ADD field_name VARCHAR(250)'); + $this->addSql('ALTER TABLE nodes_sources_documents ADD field_name VARCHAR(250)'); + $this->addSql('ALTER TABLE nodes_to_nodes ADD field_name VARCHAR(250)'); + } + + public function down(Schema $schema): void + { + $this->throwIrreversibleMigrationException('Cannot convert node-type fields name back to their identifiers'); + } +} diff --git a/migrations/Version20240318184556.php b/migrations/Version20240318184556.php new file mode 100644 index 00000000..69d87323 --- /dev/null +++ b/migrations/Version20240318184556.php @@ -0,0 +1,68 @@ +connection->beginTransaction(); + $this->connection->executeStatement('UPDATE nodes_custom_forms SET field_name = (SELECT name FROM node_type_fields WHERE id = node_type_field_id)'); + $this->connection->executeStatement('UPDATE nodes_sources_documents SET field_name = (SELECT name FROM node_type_fields WHERE id = node_type_field_id)'); + $this->connection->executeStatement('UPDATE nodes_to_nodes SET field_name = (SELECT name FROM node_type_fields WHERE id = node_type_field_id)'); + $this->connection->commit(); + + $this->addSql('ALTER TABLE nodes_custom_forms DROP FOREIGN KEY FK_4D401A0C47705282'); + $this->addSql('DROP INDEX IDX_4D401A0C47705282 ON nodes_custom_forms'); + $this->addSql('DROP INDEX customform_node_field_position ON nodes_custom_forms'); + $this->addSql('CREATE INDEX customform_node_field_position ON nodes_custom_forms (node_id, field_name, position)'); + $this->addSql('ALTER TABLE nodes_sources_documents DROP FOREIGN KEY FK_1CD104F747705282'); + $this->addSql('DROP INDEX IDX_1CD104F747705282 ON nodes_sources_documents'); + $this->addSql('DROP INDEX nsdoc_field ON nodes_sources_documents'); + $this->addSql('DROP INDEX nsdoc_field_position ON nodes_sources_documents'); + $this->addSql('CREATE INDEX nsdoc_field ON nodes_sources_documents (ns_id, field_name)'); + $this->addSql('CREATE INDEX nsdoc_field_position ON nodes_sources_documents (ns_id, field_name, position)'); + $this->addSql('ALTER TABLE nodes_to_nodes DROP FOREIGN KEY FK_761F9A9147705282'); + $this->addSql('DROP INDEX IDX_761F9A9147705282 ON nodes_to_nodes'); + $this->addSql('DROP INDEX node_a_field ON nodes_to_nodes'); + $this->addSql('DROP INDEX node_a_field_position ON nodes_to_nodes'); + $this->addSql('DROP INDEX node_b_field ON nodes_to_nodes'); + $this->addSql('DROP INDEX node_b_field_position ON nodes_to_nodes'); + $this->addSql('CREATE INDEX node_a_field ON nodes_to_nodes (node_a_id, field_name)'); + $this->addSql('CREATE INDEX node_a_field_position ON nodes_to_nodes (node_a_id, field_name, position)'); + $this->addSql('CREATE INDEX node_b_field ON nodes_to_nodes (node_b_id, field_name)'); + $this->addSql('CREATE INDEX node_b_field_position ON nodes_to_nodes (node_b_id, field_name, position)'); + + /* + * DESTRUCTIVE OPERATIONS + */ + $this->addSql('ALTER TABLE nodes_custom_forms CHANGE field_name field_name VARCHAR(250) NOT NULL'); + $this->addSql('ALTER TABLE nodes_sources_documents CHANGE field_name field_name VARCHAR(250) NOT NULL'); + $this->addSql('ALTER TABLE nodes_to_nodes CHANGE field_name field_name VARCHAR(250) NOT NULL'); + + $this->addSql('ALTER TABLE nodes_custom_forms DROP node_type_field_id'); + $this->addSql('ALTER TABLE nodes_sources_documents DROP node_type_field_id'); + $this->addSql('ALTER TABLE nodes_to_nodes DROP node_type_field_id'); + } + + public function down(Schema $schema): void + { + $this->throwIrreversibleMigrationException('Cannot convert node-type fields name back to their identifiers'); + } +} diff --git a/migrations/Version20240318204224.php b/migrations/Version20240318204224.php new file mode 100644 index 00000000..0897dc0b --- /dev/null +++ b/migrations/Version20240318204224.php @@ -0,0 +1,44 @@ +connection->executeQuery('SELECT max(length(name)) FROM `node_type_fields`'); + $maxLength = $result->fetchOne(); + + $this->skipIf(!is_numeric($maxLength), 'Cannot find node_type_fields name maximum length.'); + $this->skipIf($maxLength >= 50, 'You have at least on node_type_field name that exceed 50 characters long.'); + + // this up() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE node_type_fields CHANGE name name VARCHAR(50) NOT NULL'); + $this->addSql('CREATE INDEX ntf_name ON node_type_fields (name)'); + $this->addSql('ALTER TABLE nodes_custom_forms CHANGE field_name field_name VARCHAR(50) NOT NULL'); + $this->addSql('ALTER TABLE nodes_sources_documents CHANGE field_name field_name VARCHAR(50) NOT NULL'); + $this->addSql('ALTER TABLE nodes_to_nodes CHANGE field_name field_name VARCHAR(50) NOT NULL'); + } + + public function down(Schema $schema): void + { + $this->addSql('DROP INDEX ntf_name ON node_type_fields'); + $this->addSql('ALTER TABLE node_type_fields CHANGE name name VARCHAR(250) NOT NULL'); + $this->addSql('ALTER TABLE nodes_custom_forms CHANGE field_name field_name VARCHAR(250) NOT NULL'); + $this->addSql('ALTER TABLE nodes_sources_documents CHANGE field_name field_name VARCHAR(250) NOT NULL'); + $this->addSql('ALTER TABLE nodes_to_nodes CHANGE field_name field_name VARCHAR(250) NOT NULL'); + } +} diff --git a/migrations/Version20240603210209.php b/migrations/Version20240603210209.php new file mode 100644 index 00000000..7aa4ccd3 --- /dev/null +++ b/migrations/Version20240603210209.php @@ -0,0 +1,33 @@ +addSql('ALTER TABLE attributes ADD weight INT DEFAULT 0 NOT NULL'); + $this->addSql('CREATE INDEX IDX_319B9E707CD5541 ON attributes (weight)'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('DROP INDEX IDX_319B9E707CD5541 ON attributes'); + $this->addSql('ALTER TABLE attributes DROP weight'); + } +} diff --git a/migrations/Version20240604143759.php b/migrations/Version20240604143759.php new file mode 100644 index 00000000..da201acc --- /dev/null +++ b/migrations/Version20240604143759.php @@ -0,0 +1,33 @@ +addSql('CREATE INDEX IDX_319B9E70665648E9 ON attributes (color)'); + $this->addSql('ALTER TABLE node_types ADD attributable_by_weight TINYINT(1) DEFAULT 0 NOT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('DROP INDEX IDX_319B9E70665648E9 ON attributes'); + $this->addSql('ALTER TABLE node_types DROP attributable_by_weight'); + } +} diff --git a/phpstan.neon b/phpstan.neon index 8785b98d..882156d4 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,12 +1,17 @@ parameters: - level: 6 + level: 7 paths: - src excludePaths: - */node_modules/* - */bower_components/* - */static/* + doctrine: + repositoryClass: RZ\Roadiz\CoreBundle\Repository\EntityRepository + objectManagerLoader: ./tests/object-manager.php ignoreErrors: + - identifier: missingType.iterableValue + - identifier: missingType.generics - '#Call to an undefined method RZ\\Roadiz\\CoreBundle\\Repository#' - '#Call to an undefined method RZ\\Roadiz\\UserBundle\\Repository#' - '#Call to an undefined method Doctrine\\Persistence\\ObjectRepository#' @@ -26,9 +31,11 @@ parameters: - '#Doctrine\\ORM\\Mapping\\GeneratedValue constructor expects#' - '#type mapping mismatch: property can contain Doctrine\\Common\\Collections\\Collection]+> but database expects Doctrine\\Common\\Collections\\Collection&iterable<[^\>]+>#' - '#should return Doctrine\\Common\\Collections\\Collection]+Interface> but returns Doctrine\\Common\\Collections\\Collection]+>#' + - '#but returns Doctrine\\Common\\Collections\\ReadableCollection]+>#' + - '#does not accept Doctrine\\Common\\Collections\\ReadableCollection]+>#' reportUnmatchedIgnoredErrors: false - checkGenericClassInNonGenericObjectType: false - checkMissingIterableValueType: false + treatPhpDocTypesAsCertain: false includes: - vendor/phpstan/phpstan-doctrine/extension.neon + - vendor/phpstan/phpstan-doctrine/rules.neon diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 00000000..c0f0120e --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + src/Test + + + + + + src + + + + + + + + + + + + diff --git a/src/Api/Breadcrumbs/NodesSourcesBreadcrumbsFactory.php b/src/Api/Breadcrumbs/NodesSourcesBreadcrumbsFactory.php index 5a5e3fe9..6f02c639 100644 --- a/src/Api/Breadcrumbs/NodesSourcesBreadcrumbsFactory.php +++ b/src/Api/Breadcrumbs/NodesSourcesBreadcrumbsFactory.php @@ -20,8 +20,6 @@ public function create(?PersistableInterface $entity): ?BreadcrumbsInterface } if ( - null === $entity->getNode() || - null === $entity->getNode()->getNodeType() || !$entity->isReachable() ) { return null; diff --git a/src/Api/Controller/GetWebResponseByPathController.php b/src/Api/Controller/GetWebResponseByPathController.php index 6441d6a4..2e924a4f 100644 --- a/src/Api/Controller/GetWebResponseByPathController.php +++ b/src/Api/Controller/GetWebResponseByPathController.php @@ -4,72 +4,96 @@ namespace RZ\Roadiz\CoreBundle\Api\Controller; -use ApiPlatform\Core\Api\IriConverterInterface; -use ApiPlatform\Core\Exception\InvalidArgumentException; +use ApiPlatform\Api\IriConverterInterface; +use ApiPlatform\Exception\InvalidArgumentException; +use ApiPlatform\Exception\ResourceClassNotFoundException; +use ApiPlatform\Metadata\Exception\OperationNotFoundException; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use Psr\Log\LoggerInterface; use RZ\Roadiz\Core\AbstractEntities\PersistableInterface; use RZ\Roadiz\CoreBundle\Api\DataTransformer\WebResponseDataTransformerInterface; use RZ\Roadiz\CoreBundle\Api\Model\WebResponseInterface; +use RZ\Roadiz\CoreBundle\Entity\NodesSources; use RZ\Roadiz\CoreBundle\Entity\Redirection; +use RZ\Roadiz\CoreBundle\NodeType\ApiResourceOperationNameGenerator; use RZ\Roadiz\CoreBundle\Preview\PreviewResolverInterface; use RZ\Roadiz\CoreBundle\Routing\PathResolverInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; -use Symfony\Component\HttpFoundation\RequestStack; -use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Exception\ResourceNotFoundException; use Symfony\Component\String\UnicodeString; final class GetWebResponseByPathController extends AbstractController { - private RequestStack $requestStack; - private PathResolverInterface $pathResolver; - private WebResponseDataTransformerInterface $webResponseDataTransformer; - private IriConverterInterface $iriConverter; - private PreviewResolverInterface $previewResolver; - public function __construct( - RequestStack $requestStack, - PathResolverInterface $pathResolver, - WebResponseDataTransformerInterface $webResponseDataTransformer, - IriConverterInterface $iriConverter, - PreviewResolverInterface $previewResolver + private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, + private readonly PathResolverInterface $pathResolver, + private readonly WebResponseDataTransformerInterface $webResponseDataTransformer, + private readonly IriConverterInterface $iriConverter, + private readonly PreviewResolverInterface $previewResolver, + private readonly ApiResourceOperationNameGenerator $apiResourceOperationNameGenerator, + private readonly LoggerInterface $logger ) { - $this->requestStack = $requestStack; - $this->pathResolver = $pathResolver; - $this->webResponseDataTransformer = $webResponseDataTransformer; - $this->iriConverter = $iriConverter; - $this->previewResolver = $previewResolver; } - public function __invoke(): ?WebResponseInterface + public function __invoke(?Request $request): ?WebResponseInterface { try { if ( - null === $this->requestStack->getMainRequest() || - empty($this->requestStack->getMainRequest()->query->get('path')) + null === $request || + empty($request->query->get('path')) ) { throw new InvalidArgumentException('path query parameter is mandatory'); } $resource = $this->normalizeResourcePath( - (string) $this->requestStack->getMainRequest()->query->get('path') + $request, + (string) $request->query->get('path') ); - $this->requestStack->getMainRequest()->attributes->set('data', $resource); - $this->requestStack->getMainRequest()->attributes->set('id', $resource->getId()); - /* - * Force API Platform to look for real resource configuration and serialization - * context. You must define "itemOperations.getByPath" for your API resource configuration. - */ - $this->requestStack->getMainRequest()->attributes->set('_api_resource_class', get_class($resource)); - return $this->webResponseDataTransformer->transform($resource, WebResponseInterface::class); - } catch (ResourceNotFoundException $exception) { - throw new NotFoundHttpException($exception->getMessage(), $exception); + $request->attributes->set('id', $resource->getId()); + $request->attributes->set('path', (string) $request->query->get('path')); + $request->attributes->set('_route_params', [ + ...$request->attributes->get('_route_params', []), + 'path' => (string) $request->query->get('path'), + ]); + + try { + /* + * Force API Platform to look for real resource configuration and serialization + * context. You must define "%entity%_get_by_path" operation for your WebResponse resource configuration. + * It should be generated automatically by Roadiz when you create new reachable NodeTypes. + */ + $resourceClass = get_class($resource); + $operationName = $this->apiResourceOperationNameGenerator->generateGetByPath($resourceClass); + $webResponseClass = $request->attributes->get('_api_resource_class'); + $operation = $this->resourceMetadataCollectionFactory + ->create($webResponseClass) + ->getOperation($operationName); + $request->attributes->set('_api_operation', $operation); + $request->attributes->set('_web_response_item_class', $resourceClass); + $request->attributes->set('_api_operation_name', $operationName); + } catch (OperationNotFoundException $exception) { + // Do not fail if operation is not found + // But warn in logs about missing operation configuration for this resource + $this->logger->warning($exception->getMessage()); + } + + $request->attributes->set('_stateless', true); + + if ($resource instanceof NodesSources) { + $request->attributes->set('_translation', $resource->getTranslation()); + $request->attributes->set('_locale', $resource->getTranslation()->getPreferredLocale()); + } + + $data = $this->webResponseDataTransformer->transform($resource, WebResponseInterface::class); + $request->attributes->set('data', $data); + + return $data; + } catch (ResourceNotFoundException | ResourceClassNotFoundException $exception) { + throw $this->createNotFoundException($exception->getMessage(), $exception); } } - /** - * @param string $path - * @return PersistableInterface - */ - protected function normalizeResourcePath(string $path): PersistableInterface + protected function normalizeResourcePath(?Request $request, string $path): PersistableInterface { /* * Serve any PersistableInterface Resource by implementing @@ -104,11 +128,11 @@ protected function normalizeResourcePath(string $path): PersistableInterface * Recursive call to normalize path coming from Redirection if redirected path * is internal (starting with /) */ - return $this->normalizeResourcePath($resource->getRedirectUri()); + return $this->normalizeResourcePath($request, $resource->getRedirectUri()); } } - $this->addResourceToCacheTags($resource); + $this->addResourceToCacheTags($request, $resource); /* * Or plain entity @@ -116,12 +140,11 @@ protected function normalizeResourcePath(string $path): PersistableInterface return $resource; } - protected function addResourceToCacheTags(PersistableInterface $resource): void + protected function addResourceToCacheTags(?Request $request, PersistableInterface $resource): void { - $request = $this->requestStack->getMainRequest(); if (null !== $request) { - $iri = $this->iriConverter->getIriFromItem($resource); - $request->attributes->set('_resources', $request->attributes->get('_resources', []) + [$iri]); + $iri = $this->iriConverter->getIriFromResource($resource); + $request->attributes->set('_resources', $request->attributes->get('_resources', []) + [ $iri => $iri ]); } } } diff --git a/src/Api/Controller/TranslationAwareControllerTrait.php b/src/Api/Controller/TranslationAwareControllerTrait.php index f291948d..47533414 100644 --- a/src/Api/Controller/TranslationAwareControllerTrait.php +++ b/src/Api/Controller/TranslationAwareControllerTrait.php @@ -4,6 +4,7 @@ namespace RZ\Roadiz\CoreBundle\Api\Controller; +use Doctrine\ORM\NonUniqueResultException; use Doctrine\Persistence\ManagerRegistry; use RZ\Roadiz\Core\AbstractEntities\TranslationInterface; use RZ\Roadiz\CoreBundle\Preview\PreviewResolverInterface; @@ -16,12 +17,20 @@ trait TranslationAwareControllerTrait abstract protected function getManagerRegistry(): ManagerRegistry; abstract protected function getPreviewResolver(): PreviewResolverInterface; + /** + * @throws NonUniqueResultException + */ protected function getTranslation(Request $request): TranslationInterface { $locale = $request->query->get('_locale'); + $requestTranslation = $request->attributes->get('_translation'); + if ($requestTranslation instanceof TranslationInterface) { + return $requestTranslation; + } + /** @var TranslationRepository $repository */ $repository = $this->getManagerRegistry()->getRepository(TranslationInterface::class); - if (null === $locale) { + if (!\is_string($locale) || $locale === '') { return $repository->findDefault(); } diff --git a/src/Api/DataTransformer/AttributeOutputDataTransformer.php b/src/Api/DataTransformer/AttributeOutputDataTransformer.php deleted file mode 100644 index 15bdea15..00000000 --- a/src/Api/DataTransformer/AttributeOutputDataTransformer.php +++ /dev/null @@ -1,45 +0,0 @@ -group = $data->getGroup(); - $output->code = $data->getCode(); - $output->type = $data->getType(); - $output->color = $data->getColor(); - $output->documents = $data->getDocuments()->toArray(); - - if (isset($context['translation']) && $context['translation'] instanceof TranslationInterface) { - $output->name = $data->getLabelOrCode($context['translation']); - } - return $output; - } - - /** - * @inheritDoc - */ - public function supportsTransformation($data, string $to, array $context = []): bool - { - return AttributeOutput::class === $to && $data instanceof AttributeInterface; - } -} diff --git a/src/Api/DataTransformer/AttributeValueOutputDataTransformer.php b/src/Api/DataTransformer/AttributeValueOutputDataTransformer.php deleted file mode 100644 index 9421721b..00000000 --- a/src/Api/DataTransformer/AttributeValueOutputDataTransformer.php +++ /dev/null @@ -1,48 +0,0 @@ -attribute = $data->getAttribute(); - $output->attributable = $data->getAttributable(); - $output->type = $data->getType(); - - if (isset($context['translation']) && $context['translation'] instanceof TranslationInterface) { - $translatedData = $data->getAttributeValueTranslation($context['translation']); - $output->label = $data->getAttribute()->getLabelOrCode($context['translation']); - if ($translatedData instanceof AttributeValueTranslationInterface) { - $output->value = $translatedData->getValue(); - } - } - return $output; - } - - /** - * @inheritDoc - */ - public function supportsTransformation($data, string $to, array $context = []): bool - { - return AttributeValueOutput::class === $to && $data instanceof AttributeValueInterface; - } -} diff --git a/src/Api/DataTransformer/BaseNodesSourcesOutputDataTransformer.php b/src/Api/DataTransformer/BaseNodesSourcesOutputDataTransformer.php deleted file mode 100644 index 2465864f..00000000 --- a/src/Api/DataTransformer/BaseNodesSourcesOutputDataTransformer.php +++ /dev/null @@ -1,35 +0,0 @@ -transformNodesSources($output, $data, $context); - } - - /** - * @inheritDoc - */ - public function supportsTransformation($data, string $to, array $context = []): bool - { - return NodesSourcesOutput::class === $to && $data instanceof NodesSources; - } -} diff --git a/src/Api/DataTransformer/BlocksAwareWebResponseOutputDataTransformerTrait.php b/src/Api/DataTransformer/BlocksAwareWebResponseOutputDataTransformerTrait.php index b5be9688..2e89b12e 100644 --- a/src/Api/DataTransformer/BlocksAwareWebResponseOutputDataTransformerTrait.php +++ b/src/Api/DataTransformer/BlocksAwareWebResponseOutputDataTransformerTrait.php @@ -8,6 +8,7 @@ use RZ\Roadiz\CoreBundle\Api\Model\BlocksAwareWebResponseInterface; use RZ\Roadiz\CoreBundle\Api\Model\RealmsAwareWebResponseInterface; use RZ\Roadiz\CoreBundle\Api\Model\WebResponseInterface; +use RZ\Roadiz\CoreBundle\Api\TreeWalker\TreeWalkerGenerator; use RZ\Roadiz\CoreBundle\Entity\NodesSources; use RZ\TreeWalker\AbstractWalker; use RZ\TreeWalker\WalkerContextInterface; @@ -16,6 +17,7 @@ trait BlocksAwareWebResponseOutputDataTransformerTrait { abstract protected function getWalkerContext(): WalkerContextInterface; abstract protected function getCacheItemPool(): CacheItemPoolInterface; + abstract protected function getTreeWalkerGenerator(): TreeWalkerGenerator; abstract protected function getChildrenNodeSourceWalkerMaxLevel(): int; /** @@ -23,22 +25,17 @@ abstract protected function getChildrenNodeSourceWalkerMaxLevel(): int; */ abstract protected function getChildrenNodeSourceWalkerClassname(): string; - /** - * @param BlocksAwareWebResponseInterface $output - * @param NodesSources $data - * @return WebResponseInterface - */ protected function injectBlocks(BlocksAwareWebResponseInterface $output, NodesSources $data): WebResponseInterface { if (!$output instanceof RealmsAwareWebResponseInterface || !$output->isHidingBlocks()) { - /** @var class-string $childrenNodeSourceWalkerClassname */ - $childrenNodeSourceWalkerClassname = $this->getChildrenNodeSourceWalkerClassname(); - $output->setBlocks($childrenNodeSourceWalkerClassname::build( + $walker = $this->getTreeWalkerGenerator()->buildForRoot( $data, + $this->getChildrenNodeSourceWalkerClassname(), $this->getWalkerContext(), $this->getChildrenNodeSourceWalkerMaxLevel(), $this->getCacheItemPool() - )->getChildren()); + ); + $output->setBlocks($walker->getChildren()); } return $output; diff --git a/src/Api/DataTransformer/CustomFormOutputDataTransformer.php b/src/Api/DataTransformer/CustomFormOutputDataTransformer.php deleted file mode 100644 index c4411bca..00000000 --- a/src/Api/DataTransformer/CustomFormOutputDataTransformer.php +++ /dev/null @@ -1,59 +0,0 @@ -urlGenerator = $urlGenerator; - } - - /** - * @inheritDoc - */ - public function transform($data, string $to, array $context = []): object - { - if (!$data instanceof CustomForm) { - throw new \InvalidArgumentException('Data to transform must be instance of ' . CustomForm::class); - } - $output = new CustomFormOutput(); - $output->name = $data->getDisplayName(); - $output->color = $data->getColor(); - $output->description = $data->getDescription(); - $output->slug = (new AsciiSlugger())->slug($data->getName())->snake()->toString(); - $output->open = $data->isFormStillOpen(); - $output->definitionUrl = $this->urlGenerator->generate('api_custom_forms_item_definition', [ - 'id' => $data->getId() - ]); - $output->postUrl = $this->urlGenerator->generate('api_custom_forms_item_post', [ - 'id' => $data->getId() - ]); - - return $output; - } - - /** - * @inheritDoc - */ - public function supportsTransformation($data, string $to, array $context = []): bool - { - return CustomFormOutput::class === $to && $data instanceof CustomForm; - } -} diff --git a/src/Api/DataTransformer/DocumentOutputDataTransformer.php b/src/Api/DataTransformer/DocumentOutputDataTransformer.php deleted file mode 100644 index 7455219f..00000000 --- a/src/Api/DataTransformer/DocumentOutputDataTransformer.php +++ /dev/null @@ -1,96 +0,0 @@ -documentFinder = $documentFinder; - } - - /** - * @inheritDoc - */ - public function transform($object, string $to, array $context = []): object - { - if (!$object instanceof Document) { - throw new \InvalidArgumentException('Data to transform must be instance of ' . DocumentInterface::class); - } - $output = new DocumentOutput(); - $output->relativePath = $object->getRelativePath(); - $output->processable = $object->isProcessable(); - $output->type = $object->getShortType(); - $output->imageWidth = $object->getImageWidth(); - $output->imageHeight = $object->getImageHeight(); - $output->mimeType = $object->getMimeType(); - $output->alt = $object->getFilename(); - $output->embedId = $object->getEmbedId(); - $output->embedPlatform = $object->getEmbedPlatform(); - $output->imageAverageColor = $object->getImageAverageColor(); - $output->mediaDuration = $object->getMediaDuration(); - - /** @var array $serializationGroups */ - $serializationGroups = isset($context['groups']) && is_array($context['groups']) ? $context['groups'] : []; - - if (($object->isEmbed() || !$object->isImage()) && false !== $object->getThumbnails()->first()) { - $output->thumbnail = $object->getThumbnails()->first(); - } - - if (isset($context['translation']) && $context['translation'] instanceof TranslationInterface) { - $translatedData = $object->getDocumentTranslationsByTranslation($context['translation'])->first() ?: null; - if ($translatedData instanceof DocumentTranslation) { - $output->name = $translatedData->getName(); - $output->description = $translatedData->getDescription(); - $output->copyright = $translatedData->getCopyright(); - $output->alt = !empty($translatedData->getName()) ? $translatedData->getName() : $object->getFilename(); - $output->externalUrl = $translatedData->getExternalUrl(); - } - } - - if (in_array('document_folders', $serializationGroups)) { - $output->folders = $object->getFolders()->toArray(); - } - - if (in_array('document_display_sources', $serializationGroups)) { - if ($object->isLocal() && $object->isVideo()) { - foreach ($this->documentFinder->findVideosWithFilename($object->getRelativePath()) as $document) { - if ($document->getRelativePath() !== $object->getRelativePath()) { - $output->altSources[] = $document; - } - } - } elseif ($object->isLocal() && $object->isAudio()) { - foreach ($this->documentFinder->findAudiosWithFilename($object->getRelativePath()) as $document) { - if ($document->getRelativePath() !== $object->getRelativePath()) { - $output->altSources[] = $document; - } - } - } - } - - return $output; - } - - /** - * @inheritDoc - */ - public function supportsTransformation($data, string $to, array $context = []): bool - { - return DocumentOutput::class === $to && $data instanceof DocumentInterface; - } -} diff --git a/src/Api/DataTransformer/FolderOutputDataTransformer.php b/src/Api/DataTransformer/FolderOutputDataTransformer.php deleted file mode 100644 index 63d8b79b..00000000 --- a/src/Api/DataTransformer/FolderOutputDataTransformer.php +++ /dev/null @@ -1,49 +0,0 @@ -name = $data->getName(); - $output->slug = $data->getFolderName(); - $output->visible = $data->getVisible(); - $output->position = $data->getPosition(); - - if (isset($context['translation']) && $context['translation'] instanceof TranslationInterface) { - $translatedData = $data->getTranslatedFoldersByTranslation($context['translation'])->first() ?: null; - if ($translatedData instanceof FolderTranslation) { - $output->name = $translatedData->getName(); - } - } - return $output; - } - - /** - * @inheritDoc - */ - public function supportsTransformation($data, string $to, array $context = []): bool - { - return FolderOutput::class === $to && $data instanceof FolderInterface; - } -} diff --git a/src/Api/DataTransformer/NodeOutputDataTransformer.php b/src/Api/DataTransformer/NodeOutputDataTransformer.php deleted file mode 100644 index 3dec987d..00000000 --- a/src/Api/DataTransformer/NodeOutputDataTransformer.php +++ /dev/null @@ -1,39 +0,0 @@ -nodeName = $data->getNodeName(); - $output->visible = $data->isVisible(); - $output->position = $data->getPosition(); - $output->tags = $data->getTags()->toArray(); - return $output; - } - - /** - * @inheritDoc - */ - public function supportsTransformation($data, string $to, array $context = []): bool - { - return NodeOutput::class === $to && $data instanceof Node; - } -} diff --git a/src/Api/DataTransformer/NodesSourcesOutputDataTransformer.php b/src/Api/DataTransformer/NodesSourcesOutputDataTransformer.php deleted file mode 100644 index 03c85bbd..00000000 --- a/src/Api/DataTransformer/NodesSourcesOutputDataTransformer.php +++ /dev/null @@ -1,52 +0,0 @@ -urlGenerator = $urlGenerator; - } - - protected function transformNodesSources( - NodesSourcesDto $output, - NodesSources $data, - array $context = [] - ): NodesSourcesDto { - $output->title = $data->getTitle(); - $output->node = $data->getNode(); - $output->metaTitle = $data->getMetaTitle(); - $output->metaDescription = $data->getMetaDescription(); - $output->translation = $data->getTranslation(); - $output->slug = $data->getIdentifier(); - if ($data->isPublishable()) { - $output->publishedAt = $data->getPublishedAt(); - } - return $output; - } - - /** - * @inheritDoc - */ - public function supportsTransformation($data, string $to, array $context = []): bool - { - return $data instanceof NodesSources; - } -} diff --git a/src/Api/DataTransformer/TagOutputDataTransformer.php b/src/Api/DataTransformer/TagOutputDataTransformer.php deleted file mode 100644 index 947a0acc..00000000 --- a/src/Api/DataTransformer/TagOutputDataTransformer.php +++ /dev/null @@ -1,53 +0,0 @@ -slug = $data->getTagName(); - $output->color = $data->getColor(); - $output->visible = $data->isVisible(); - $output->position = $data->getPosition(); - if ($data->getParent() instanceof Tag) { - $output->parent = $data->getParent(); - } - - if (isset($context['translation']) && $context['translation'] instanceof TranslationInterface) { - $translatedData = $data->getTranslatedTagsByTranslation($context['translation'])->first() ?: null; - if ($translatedData instanceof TagTranslation) { - $output->name = $translatedData->getName(); - $output->description = $translatedData->getDescription(); - $output->documents = $translatedData->getDocuments(); - } - } - return $output; - } - - /** - * @inheritDoc - */ - public function supportsTransformation($data, string $to, array $context = []): bool - { - return TagOutput::class === $to && $data instanceof Tag; - } -} diff --git a/src/Api/DataTransformer/TranslationOutputDataTransformer.php b/src/Api/DataTransformer/TranslationOutputDataTransformer.php deleted file mode 100644 index 41f6d702..00000000 --- a/src/Api/DataTransformer/TranslationOutputDataTransformer.php +++ /dev/null @@ -1,36 +0,0 @@ -locale = $data->getPreferredLocale(); - $output->defaultTranslation = $data->isDefaultTranslation(); - $output->available = $data->isAvailable(); - $output->name = $data->getName(); - return $output; - } - - /** - * @inheritDoc - */ - public function supportsTransformation($data, string $to, array $context = []): bool - { - return TranslationOutput::class === $to && $data instanceof TranslationInterface; - } -} diff --git a/src/Api/DataTransformer/WebResponseDataTransformerInterface.php b/src/Api/DataTransformer/WebResponseDataTransformerInterface.php index a6d97c0f..424d02a3 100644 --- a/src/Api/DataTransformer/WebResponseDataTransformerInterface.php +++ b/src/Api/DataTransformer/WebResponseDataTransformerInterface.php @@ -4,25 +4,19 @@ namespace RZ\Roadiz\CoreBundle\Api\DataTransformer; +use RZ\Roadiz\Core\AbstractEntities\PersistableInterface; use RZ\Roadiz\CoreBundle\Api\Model\WebResponseInterface; interface WebResponseDataTransformerInterface { /** - * Transforms the given object to something else, usually another object. - * This must return the original object if no transformations have been done. - * - * @param object $object + * @template T of PersistableInterface + * @param T $object * @param string $to * @param array $context - * @return WebResponseInterface|null + * @return WebResponseInterface|null */ - public function transform($object, string $to, array $context = []): ?WebResponseInterface; + public function transform(PersistableInterface $object, string $to, array $context = []): ?WebResponseInterface; - /** - * Checks whether the transformation is supported for a given data and context. - * - * @param object|array $data object on normalize / array on denormalize - */ - public function supportsTransformation($data, string $to, array $context = []): bool; + public function createWebResponse(): WebResponseInterface; } diff --git a/src/Api/DataTransformer/WebResponseOutputDataTransformer.php b/src/Api/DataTransformer/WebResponseOutputDataTransformer.php index 950eeef1..7cd9a1e1 100644 --- a/src/Api/DataTransformer/WebResponseOutputDataTransformer.php +++ b/src/Api/DataTransformer/WebResponseOutputDataTransformer.php @@ -8,12 +8,15 @@ use RZ\Roadiz\Core\AbstractEntities\PersistableInterface; use RZ\Roadiz\Core\AbstractEntities\TranslationInterface; use RZ\Roadiz\CoreBundle\Api\Breadcrumbs\BreadcrumbsFactoryInterface; +use RZ\Roadiz\CoreBundle\Api\Model\BlocksAwareWebResponseInterface; use RZ\Roadiz\CoreBundle\Api\Model\NodesSourcesHeadFactoryInterface; -use RZ\Roadiz\CoreBundle\Api\Model\WebResponse; +use RZ\Roadiz\CoreBundle\Api\Model\RealmsAwareWebResponseInterface; use RZ\Roadiz\CoreBundle\Api\Model\WebResponseInterface; use RZ\Roadiz\CoreBundle\Api\TreeWalker\AutoChildrenNodeSourceWalker; +use RZ\Roadiz\CoreBundle\Api\TreeWalker\TreeWalkerGenerator; use RZ\Roadiz\CoreBundle\Entity\NodesSources; use RZ\Roadiz\CoreBundle\Realm\RealmResolverInterface; +use RZ\TreeWalker\AbstractWalker; use RZ\TreeWalker\WalkerContextInterface; use Symfony\Cmf\Component\Routing\RouteObjectInterface; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; @@ -23,27 +26,26 @@ class WebResponseOutputDataTransformer implements WebResponseDataTransformerInte use BlocksAwareWebResponseOutputDataTransformerTrait; use RealmsAwareWebResponseOutputDataTransformerTrait; - private NodesSourcesHeadFactoryInterface $nodesSourcesHeadFactory; - private BreadcrumbsFactoryInterface $breadcrumbsFactory; - private WalkerContextInterface $walkerContext; - private CacheItemPoolInterface $cacheItemPool; - private UrlGeneratorInterface $urlGenerator; - private RealmResolverInterface $realmResolver; - + /** + * @param NodesSourcesHeadFactoryInterface $nodesSourcesHeadFactory + * @param BreadcrumbsFactoryInterface $breadcrumbsFactory + * @param WalkerContextInterface $walkerContext + * @param CacheItemPoolInterface $cacheItemPool + * @param UrlGeneratorInterface $urlGenerator + * @param RealmResolverInterface $realmResolver + * @param TreeWalkerGenerator $treeWalkerGenerator + * @param class-string $webResponseClass + */ public function __construct( - NodesSourcesHeadFactoryInterface $nodesSourcesHeadFactory, - BreadcrumbsFactoryInterface $breadcrumbsFactory, - WalkerContextInterface $walkerContext, - CacheItemPoolInterface $cacheItemPool, - UrlGeneratorInterface $urlGenerator, - RealmResolverInterface $realmResolver + protected readonly NodesSourcesHeadFactoryInterface $nodesSourcesHeadFactory, + protected readonly BreadcrumbsFactoryInterface $breadcrumbsFactory, + protected readonly WalkerContextInterface $walkerContext, + protected readonly CacheItemPoolInterface $cacheItemPool, + protected readonly UrlGeneratorInterface $urlGenerator, + protected readonly RealmResolverInterface $realmResolver, + protected readonly TreeWalkerGenerator $treeWalkerGenerator, + private readonly string $webResponseClass ) { - $this->nodesSourcesHeadFactory = $nodesSourcesHeadFactory; - $this->breadcrumbsFactory = $breadcrumbsFactory; - $this->walkerContext = $walkerContext; - $this->cacheItemPool = $cacheItemPool; - $this->urlGenerator = $urlGenerator; - $this->realmResolver = $realmResolver; } protected function getWalkerContext(): WalkerContextInterface @@ -61,6 +63,14 @@ protected function getChildrenNodeSourceWalkerMaxLevel(): int return 5; } + public function getTreeWalkerGenerator(): TreeWalkerGenerator + { + return $this->treeWalkerGenerator; + } + + /** + * @return class-string + */ protected function getChildrenNodeSourceWalkerClassname(): string { return AutoChildrenNodeSourceWalker::class; @@ -71,40 +81,33 @@ protected function getRealmResolver(): RealmResolverInterface return $this->realmResolver; } - /** - * @inheritDoc - */ - public function transform($object, string $to, array $context = []): ?WebResponseInterface + public function createWebResponse(): WebResponseInterface { - if (!$object instanceof PersistableInterface) { - throw new \InvalidArgumentException( - 'Data to transform must be instance of ' . - PersistableInterface::class - ); - } - $output = new WebResponse(); - $output->item = $object; + return new ($this->webResponseClass)(); + } + + public function transform(PersistableInterface $object, string $to, array $context = []): ?WebResponseInterface + { + $output = $this->createWebResponse(); + $output->setItem($object); if ($object instanceof NodesSources) { - $this->injectRealms($output, $object); - $this->injectBlocks($output, $object); + if ($output instanceof RealmsAwareWebResponseInterface) { + $this->injectRealms($output, $object); + } + if ($output instanceof BlocksAwareWebResponseInterface) { + $this->injectBlocks($output, $object); + } - $output->path = $this->urlGenerator->generate(RouteObjectInterface::OBJECT_BASED_ROUTE_NAME, [ + $output->setPath($this->urlGenerator->generate(RouteObjectInterface::OBJECT_BASED_ROUTE_NAME, [ RouteObjectInterface::ROUTE_OBJECT => $object - ], UrlGeneratorInterface::ABSOLUTE_PATH); - $output->head = $this->nodesSourcesHeadFactory->createForNodeSource($object); - $output->breadcrumbs = $this->breadcrumbsFactory->create($object); + ], UrlGeneratorInterface::ABSOLUTE_PATH)); + $output->setHead($this->nodesSourcesHeadFactory->createForNodeSource($object)); + $output->setBreadcrumbs($this->breadcrumbsFactory->create($object)); + $output->setMaxAge($object->getNode()->getTtl() * 60); } if ($object instanceof TranslationInterface) { - $output->head = $this->nodesSourcesHeadFactory->createForTranslation($object); + $output->setHead($this->nodesSourcesHeadFactory->createForTranslation($object)); } return $output; } - - /** - * @inheritDoc - */ - public function supportsTransformation($data, string $to, array $context = []): bool - { - return WebResponseInterface::class === $to && $data instanceof PersistableInterface; - } } diff --git a/src/Api/Dto/AttributeOutput.php b/src/Api/Dto/AttributeOutput.php deleted file mode 100644 index 4ed39205..00000000 --- a/src/Api/Dto/AttributeOutput.php +++ /dev/null @@ -1,51 +0,0 @@ - - */ - #[Groups(['attribute'])] - public array $documents = []; -} diff --git a/src/Api/Dto/AttributeValueOutput.php b/src/Api/Dto/AttributeValueOutput.php deleted file mode 100644 index 9c1457eb..00000000 --- a/src/Api/Dto/AttributeValueOutput.php +++ /dev/null @@ -1,41 +0,0 @@ - - */ - #[Groups(['document_folders'])] - public array $folders = []; - /** - * @var array - */ - #[Groups(['document_display_sources'])] - #[MaxDepth(1)] - public array $altSources = []; - /** - * @var string|null - */ - #[Groups(['document', 'document_display'])] - public ?string $alt = null; -} diff --git a/src/Api/Dto/FolderOutput.php b/src/Api/Dto/FolderOutput.php deleted file mode 100644 index cd152ed1..00000000 --- a/src/Api/Dto/FolderOutput.php +++ /dev/null @@ -1,34 +0,0 @@ - - */ - #[Groups(['tag', 'tag_base'])] - public array $documents = []; - /** - * @var Tag|null - */ - #[Groups(['tag', 'tag_base'])] - #[MaxDepth(1)] - public ?Tag $parent = null; - /** - * @Groups({"tag", "tag_base"}) - */ - public ?float $position = null; -} diff --git a/src/Api/Dto/TranslationOutput.php b/src/Api/Dto/TranslationOutput.php deleted file mode 100644 index 0abd4630..00000000 --- a/src/Api/Dto/TranslationOutput.php +++ /dev/null @@ -1,34 +0,0 @@ -resourceMetadataFactory = $resourceMetadataFactory; $this->requestStack = $requestStack; $this->defaultPublicationFieldName = $defaultPublicationFieldName; } @@ -63,9 +58,10 @@ public function applyToCollection( QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, - string $operationName = null + ?Operation $operation = null, + array $context = [] ): void { - if (!$this->supportsResult($resourceClass, $operationName)) { + if (!$this->supportsResult($resourceClass, $operation)) { return; } if (null === $request = $this->requestStack->getCurrentRequest()) { @@ -73,7 +69,7 @@ public function applyToCollection( } $aliases = $queryBuilder->getRootAliases(); $alias = reset($aliases); - $publicationFieldName = $this->getPublicationFieldName($request, $this->resourceMetadataFactory->create($resourceClass), $operationName); + $publicationFieldName = $this->getPublicationFieldName($operation); $publicationField = $alias . '.' . $publicationFieldName; $queryBuilder->select($publicationField) @@ -81,17 +77,21 @@ public function applyToCollection( ->orderBy($publicationField, 'DESC'); } - public function supportsResult(string $resourceClass, string $operationName = null): bool + public function supportsResult(string $resourceClass, ?Operation $operation = null, array $context = []): bool { if (null === $request = $this->requestStack->getCurrentRequest()) { return false; } - return $this->isArchiveEnabled($request, $this->resourceMetadataFactory->create($resourceClass), $operationName); + return $this->isArchiveEnabled($operation); } - public function getResult(QueryBuilder $queryBuilder): iterable - { + public function getResult( + QueryBuilder $queryBuilder, + ?string $resourceClass = null, + ?Operation $operation = null, + array $context = [] + ): iterable { $entities = []; $dates = []; $paginator = new Paginator($queryBuilder, false); @@ -104,7 +104,7 @@ public function getResult(QueryBuilder $queryBuilder): iterable foreach ($paginator as $result) { $dateTimeField = reset($result); - if ($dateTimeField instanceof \DateTime) { + if ($dateTimeField instanceof \DateTimeInterface) { $year = $dateTimeField->format('Y'); $month = $dateTimeField->format('Y-m'); @@ -128,28 +128,14 @@ public function getResult(QueryBuilder $queryBuilder): iterable } private function isArchiveEnabled( - Request $request, - ResourceMetadata $resourceMetadata, - string $operationName = null + ?Operation $operation = null ): bool { - return $resourceMetadata->getCollectionOperationAttribute( - $operationName, - 'archive_enabled', - false, - true - ); + return $operation->getExtraProperties()['archive_enabled'] ?? false; } private function getPublicationFieldName( - Request $request, - ResourceMetadata $resourceMetadata, - string $operationName = null + ?Operation $operation = null ): string { - return $resourceMetadata->getCollectionOperationAttribute( - $operationName, - 'archive_publication_field_name', - $this->defaultPublicationFieldName, - true - ); + return $operation->getExtraProperties()['archive_publication_field_name'] ?? $this->defaultPublicationFieldName; } } diff --git a/src/Api/Extension/AttributeValueQueryExtension.php b/src/Api/Extension/AttributeValueQueryExtension.php new file mode 100644 index 00000000..3a095af7 --- /dev/null +++ b/src/Api/Extension/AttributeValueQueryExtension.php @@ -0,0 +1,84 @@ +previewResolver = $previewResolver; + } + + public function applyToItem( + QueryBuilder $queryBuilder, + QueryNameGeneratorInterface $queryNameGenerator, + string $resourceClass, + array $identifiers, + ?Operation $operation = null, + array $context = [] + ): void { + $this->apply($queryBuilder, $resourceClass); + } + + public function applyToCollection( + QueryBuilder $queryBuilder, + QueryNameGeneratorInterface $queryNameGenerator, + string $resourceClass, + ?Operation $operation = null, + array $context = [] + ): void { + $this->apply($queryBuilder, $resourceClass); + } + + private function apply( + QueryBuilder $queryBuilder, + string $resourceClass + ): void { + if ( + $resourceClass !== AttributeValue::class + ) { + return; + } + + $rootAlias = $queryBuilder->getRootAliases()[0]; + + /** + * AttributeValue is always linked to a Node. + * We need to join Node to filter by its status. + */ + $existingNodeJoin = QueryBuilderHelper::getExistingJoin($queryBuilder, 'o', 'node'); + if (null === $existingNodeJoin || !$existingNodeJoin->getAlias()) { + $queryBuilder->leftJoin($rootAlias . '.node', 'node'); + $joinAlias = 'node'; + } else { + $joinAlias = $existingNodeJoin->getAlias(); + } + + if ($this->previewResolver->isPreview()) { + $queryBuilder + ->andWhere($queryBuilder->expr()->lte($joinAlias . '.status', ':status')) + ->setParameter(':status', Node::PUBLISHED); + return; + } + + $queryBuilder + ->andWhere($queryBuilder->expr()->eq($joinAlias . '.status', ':status')) + ->setParameter(':status', Node::PUBLISHED); + return; + } +} diff --git a/src/Api/Extension/AttributeValueRealmExtension.php b/src/Api/Extension/AttributeValueRealmExtension.php new file mode 100644 index 00000000..fb8f06c9 --- /dev/null +++ b/src/Api/Extension/AttributeValueRealmExtension.php @@ -0,0 +1,69 @@ +addWhere($queryBuilder, $resourceClass); + } + + public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, Operation $operation = null, array $context = []): void + { + $this->addWhere($queryBuilder, $resourceClass); + } + + private function addWhere(QueryBuilder $queryBuilder, string $resourceClass): void + { + if ($resourceClass !== AttributeValue::class || $this->security->isGranted('ROLE_ACCESS_NODE_ATTRIBUTES')) { + return; + } + + /* + * Filter out all attribute values requiring a realm for anonymous users. + */ + $rootAlias = $queryBuilder->getRootAliases()[0]; + if ($this->security->isGranted('IS_ANONYMOUS')) { + $queryBuilder->andWhere($queryBuilder->expr()->isNull(sprintf('%s.realm', $rootAlias))); + return; + } + + /* + * Filter all attribute values requiring a granted realm or no realm for current user. + */ + $queryBuilder->andWhere($queryBuilder->expr()->orX( + $queryBuilder->expr()->isNull(sprintf('%s.realm', $rootAlias)), + $queryBuilder->expr()->in( + sprintf('%s.realm', $rootAlias), + ':realmIds' + ) + ))->setParameter('realmIds', $this->getGrantedRealmIds()); + } + + private function getGrantedRealmIds(): array + { + return array_map( + fn (RealmInterface $realm) => $realm->getId(), + array_filter($this->realmResolver->getGrantedRealms()) + ); + } +} diff --git a/src/Api/Extension/DocumentQueryExtension.php b/src/Api/Extension/DocumentQueryExtension.php index 8b4b0611..7d3894da 100644 --- a/src/Api/Extension/DocumentQueryExtension.php +++ b/src/Api/Extension/DocumentQueryExtension.php @@ -4,9 +4,10 @@ namespace RZ\Roadiz\CoreBundle\Api\Extension; -use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryCollectionExtensionInterface; -use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryItemExtensionInterface; -use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface; +use ApiPlatform\Doctrine\Orm\Extension\QueryCollectionExtensionInterface; +use ApiPlatform\Doctrine\Orm\Extension\QueryItemExtensionInterface; +use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; +use ApiPlatform\Metadata\Operation; use Doctrine\ORM\QueryBuilder; use RZ\Roadiz\CoreBundle\Entity\Document; @@ -15,8 +16,7 @@ final class DocumentQueryExtension implements QueryItemExtensionInterface, Query private function apply( QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, - string $resourceClass, - string $operationName = null + string $resourceClass ): void { if ($resourceClass !== Document::class) { return; @@ -32,18 +32,19 @@ public function applyToItem( QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, - string $operationName = null, + Operation $operation = null, array $context = [] ): void { - $this->apply($queryBuilder, $queryNameGenerator, $resourceClass, $operationName); + $this->apply($queryBuilder, $queryNameGenerator, $resourceClass); } public function applyToCollection( QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, - string $operationName = null + Operation $operation = null, + array $context = [] ): void { - $this->apply($queryBuilder, $queryNameGenerator, $resourceClass, $operationName); + $this->apply($queryBuilder, $queryNameGenerator, $resourceClass); } } diff --git a/src/Api/Extension/NodeQueryExtension.php b/src/Api/Extension/NodeQueryExtension.php index 7b259f71..0b15784e 100644 --- a/src/Api/Extension/NodeQueryExtension.php +++ b/src/Api/Extension/NodeQueryExtension.php @@ -4,10 +4,11 @@ namespace RZ\Roadiz\CoreBundle\Api\Extension; -use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryCollectionExtensionInterface; -use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryItemExtensionInterface; -use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryBuilderHelper; -use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface; +use ApiPlatform\Doctrine\Orm\Extension\QueryCollectionExtensionInterface; +use ApiPlatform\Doctrine\Orm\Extension\QueryItemExtensionInterface; +use ApiPlatform\Doctrine\Orm\Util\QueryBuilderHelper; +use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; +use ApiPlatform\Metadata\Operation; use Doctrine\ORM\Query\Expr\Join; use Doctrine\ORM\QueryBuilder; use RZ\Roadiz\CoreBundle\Entity\Node; @@ -28,17 +29,16 @@ public function applyToItem( QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, - string $operationName = null, + Operation $operation = null, array $context = [] ): void { - $this->apply($queryBuilder, $queryNameGenerator, $resourceClass, $operationName); + $this->apply($queryBuilder, $queryNameGenerator, $resourceClass); } private function apply( QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, - string $resourceClass, - string $operationName = null + string $resourceClass ): void { if ($resourceClass !== Node::class) { return; @@ -70,8 +70,9 @@ public function applyToCollection( QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, - string $operationName = null + Operation $operation = null, + array $context = [] ): void { - $this->apply($queryBuilder, $queryNameGenerator, $resourceClass, $operationName); + $this->apply($queryBuilder, $queryNameGenerator, $resourceClass); } } diff --git a/src/Api/Extension/NodesSourcesQueryExtension.php b/src/Api/Extension/NodesSourcesQueryExtension.php index 2239df4f..2e5b3960 100644 --- a/src/Api/Extension/NodesSourcesQueryExtension.php +++ b/src/Api/Extension/NodesSourcesQueryExtension.php @@ -4,10 +4,11 @@ namespace RZ\Roadiz\CoreBundle\Api\Extension; -use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryCollectionExtensionInterface; -use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryItemExtensionInterface; -use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryBuilderHelper; -use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface; +use ApiPlatform\Doctrine\Orm\Extension\QueryCollectionExtensionInterface; +use ApiPlatform\Doctrine\Orm\Extension\QueryItemExtensionInterface; +use ApiPlatform\Doctrine\Orm\Util\QueryBuilderHelper; +use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; +use ApiPlatform\Metadata\Operation; use Doctrine\ORM\Query\Expr\Join; use Doctrine\ORM\QueryBuilder; use RZ\Roadiz\CoreBundle\Entity\Node; @@ -32,26 +33,26 @@ public function applyToItem( QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, - string $operationName = null, + ?Operation $operation = null, array $context = [] ): void { - $this->apply($queryBuilder, $queryNameGenerator, $resourceClass, $operationName); + $this->apply($queryBuilder, $queryNameGenerator, $resourceClass); } public function applyToCollection( QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, - string $operationName = null + ?Operation $operation = null, + array $context = [] ): void { - $this->apply($queryBuilder, $queryNameGenerator, $resourceClass, $operationName); + $this->apply($queryBuilder, $queryNameGenerator, $resourceClass); } private function apply( QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, - string $resourceClass, - string $operationName = null + string $resourceClass ): void { if ( $resourceClass !== NodesSources::class && diff --git a/src/Api/Extension/NodesTagsQueryExtension.php b/src/Api/Extension/NodesTagsQueryExtension.php new file mode 100644 index 00000000..a5ea3cb3 --- /dev/null +++ b/src/Api/Extension/NodesTagsQueryExtension.php @@ -0,0 +1,89 @@ +previewResolver = $previewResolver; + } + + public function applyToItem( + QueryBuilder $queryBuilder, + QueryNameGeneratorInterface $queryNameGenerator, + string $resourceClass, + array $identifiers, + ?Operation $operation = null, + array $context = [] + ): void { + $this->apply($queryBuilder, $resourceClass); + } + + public function applyToCollection( + QueryBuilder $queryBuilder, + QueryNameGeneratorInterface $queryNameGenerator, + string $resourceClass, + ?Operation $operation = null, + array $context = [] + ): void { + $this->apply($queryBuilder, $resourceClass); + } + + private function apply( + QueryBuilder $queryBuilder, + string $resourceClass + ): void { + if ( + $resourceClass !== Tag::class + ) { + return; + } + + $parts = $queryBuilder->getDQLPart('join'); + $rootAlias = $queryBuilder->getRootAliases()[0]; + if (!\is_array($parts) || !isset($parts[$rootAlias])) { + return; + } + + $existingJoin = QueryBuilderHelper::getExistingJoin($queryBuilder, 'o', 'nodesTags'); + if (null === $existingJoin || !$existingJoin->getAlias()) { + return; + } + $existingNodeJoin = QueryBuilderHelper::getExistingJoin( + $queryBuilder, + $existingJoin->getAlias(), + 'node' + ); + if (null === $existingNodeJoin || !$existingNodeJoin->getAlias()) { + return; + } + + if ($this->previewResolver->isPreview()) { + $queryBuilder + ->andWhere($queryBuilder->expr()->lte($existingNodeJoin->getAlias() . '.status', ':status')) + ->setParameter(':status', Node::PUBLISHED); + return; + } + + $queryBuilder + ->andWhere($queryBuilder->expr()->eq($existingNodeJoin->getAlias() . '.status', ':status')) + ->setParameter(':status', Node::PUBLISHED); + return; + } +} diff --git a/src/Api/Filter/ArchiveFilter.php b/src/Api/Filter/ArchiveFilter.php index a7cf3fc6..7b58e2f5 100644 --- a/src/Api/Filter/ArchiveFilter.php +++ b/src/Api/Filter/ArchiveFilter.php @@ -4,14 +4,16 @@ namespace RZ\Roadiz\CoreBundle\Api\Filter; -use ApiPlatform\Core\Bridge\Doctrine\Common\PropertyHelperTrait; -use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\AbstractContextAwareFilter; -use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\DateFilter; -use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface; +use ApiPlatform\Doctrine\Common\PropertyHelperTrait; +use ApiPlatform\Doctrine\Orm\Filter\AbstractFilter; +use ApiPlatform\Doctrine\Orm\Filter\DateFilter; +use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; use ApiPlatform\Exception\FilterValidationException; +use ApiPlatform\Metadata\Operation; +use Doctrine\ORM\Query\Expr\Join; use Doctrine\ORM\QueryBuilder; -final class ArchiveFilter extends AbstractContextAwareFilter +final class ArchiveFilter extends AbstractFilter { use PropertyHelperTrait; @@ -37,18 +39,19 @@ protected function isDateField(string $property, string $resourceClass): bool */ protected function filterProperty( string $property, - $values, + mixed $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, - string $operationName = null + ?Operation $operation = null, + array $context = [] ): void { // Expect $values to be an array having the period as keys and the date value as values if ( !$this->isPropertyEnabled($property, $resourceClass) || !$this->isPropertyMapped($property, $resourceClass) || !$this->isDateField($property, $resourceClass) || - !isset($values[self::PARAMETER_ARCHIVE]) + !isset($value[self::PARAMETER_ARCHIVE]) ) { return; } @@ -57,17 +60,24 @@ protected function filterProperty( $field = $property; if ($this->isPropertyNested($property, $resourceClass)) { - [$alias, $field] = $this->addJoinsForNestedProperty($property, $alias, $queryBuilder, $queryNameGenerator, $resourceClass); + [$alias, $field] = $this->addJoinsForNestedProperty( + $property, + $alias, + $queryBuilder, + $queryNameGenerator, + $resourceClass, + Join::INNER_JOIN + ); } - if (!is_string($values[self::PARAMETER_ARCHIVE])) { + if (!is_string($value[self::PARAMETER_ARCHIVE])) { throw new FilterValidationException([sprintf( '“%s” filter must be only used with a string value.', self::PARAMETER_ARCHIVE )]); } - $range = $this->normalizeFilteringDates($values[self::PARAMETER_ARCHIVE]); + $range = $this->normalizeFilteringDates($value[self::PARAMETER_ARCHIVE]); if (null === $range || count($range) !== 2) { return; diff --git a/src/Api/Filter/CopyrightValidFilter.php b/src/Api/Filter/CopyrightValidFilter.php index 513ba4fd..ea8f56f2 100644 --- a/src/Api/Filter/CopyrightValidFilter.php +++ b/src/Api/Filter/CopyrightValidFilter.php @@ -4,12 +4,13 @@ namespace RZ\Roadiz\CoreBundle\Api\Filter; -use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\AbstractContextAwareFilter; -use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface; +use ApiPlatform\Doctrine\Orm\Filter\AbstractFilter; +use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; +use ApiPlatform\Metadata\Operation; use Doctrine\ORM\QueryBuilder; use RZ\Roadiz\CoreBundle\Entity\Document; -final class CopyrightValidFilter extends AbstractContextAwareFilter +final class CopyrightValidFilter extends AbstractFilter { public const PARAMETER = 'copyrightValid'; public const TRUE_VALUES = [1, '1', 'true', true, 'on', 'yes']; @@ -17,11 +18,12 @@ final class CopyrightValidFilter extends AbstractContextAwareFilter protected function filterProperty( string $property, - $value, + mixed $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, - string $operationName = null + ?Operation $operation = null, + array $context = [] ): void { if ($property !== self::PARAMETER) { return; diff --git a/src/Api/Filter/GeneratedEntityFilter.php b/src/Api/Filter/GeneratedEntityFilter.php index d34cb712..255be66d 100644 --- a/src/Api/Filter/GeneratedEntityFilter.php +++ b/src/Api/Filter/GeneratedEntityFilter.php @@ -4,30 +4,30 @@ namespace RZ\Roadiz\CoreBundle\Api\Filter; -use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\AbstractContextAwareFilter; +use ApiPlatform\Doctrine\Orm\Filter\AbstractFilter; use Doctrine\Persistence\ManagerRegistry; use Psr\Log\LoggerInterface; -use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; -abstract class GeneratedEntityFilter extends AbstractContextAwareFilter +abstract class GeneratedEntityFilter extends AbstractFilter { private string $generatedEntityNamespacePattern; /** * @param ManagerRegistry $managerRegistry - * @param RequestStack|null $requestStack - * @param string $generatedEntityNamespacePattern * @param LoggerInterface|null $logger * @param array|null $properties + * @param NameConverterInterface|null $nameConverter + * @param string $generatedEntityNamespacePattern */ public function __construct( ManagerRegistry $managerRegistry, - ?RequestStack $requestStack = null, - string $generatedEntityNamespacePattern = '#^App\\\GeneratedEntity\\\NS(?:[a-zA-Z]+)$#', LoggerInterface $logger = null, - array $properties = null + array $properties = null, + NameConverterInterface $nameConverter = null, + string $generatedEntityNamespacePattern = '#^App\\\GeneratedEntity\\\NS(?:[a-zA-Z]+)$#', ) { - parent::__construct($managerRegistry, $requestStack, $logger, $properties); + parent::__construct($managerRegistry, $logger, $properties, $nameConverter); $this->generatedEntityNamespacePattern = $generatedEntityNamespacePattern; } diff --git a/src/Api/Filter/IntersectionFilter.php b/src/Api/Filter/IntersectionFilter.php index 10fb2d06..699e1edc 100644 --- a/src/Api/Filter/IntersectionFilter.php +++ b/src/Api/Filter/IntersectionFilter.php @@ -4,16 +4,17 @@ namespace RZ\Roadiz\CoreBundle\Api\Filter; -use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\AbstractContextAwareFilter; -use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface; +use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; +use ApiPlatform\Doctrine\Orm\Filter\AbstractFilter; use ApiPlatform\Exception\FilterValidationException; +use ApiPlatform\Metadata\Operation; use Doctrine\ORM\Query\Expr\Join; use Doctrine\ORM\QueryBuilder; /** * Intersection filter must be used AFTER SearchFilter if you want to combine both. */ -final class IntersectionFilter extends AbstractContextAwareFilter +final class IntersectionFilter extends AbstractFilter { public const PARAMETER = 'intersect'; @@ -22,11 +23,12 @@ final class IntersectionFilter extends AbstractContextAwareFilter */ protected function filterProperty( string $property, - $value, + mixed $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, - string $operationName = null + ?Operation $operation = null, + array $context = [] ): void { if ($property !== IntersectionFilter::PARAMETER || !is_array($value)) { return; diff --git a/src/Api/Filter/LocaleFilter.php b/src/Api/Filter/LocaleFilter.php index 95da674d..a4579ec5 100644 --- a/src/Api/Filter/LocaleFilter.php +++ b/src/Api/Filter/LocaleFilter.php @@ -4,15 +4,16 @@ namespace RZ\Roadiz\CoreBundle\Api\Filter; -use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface; +use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; use ApiPlatform\Exception\FilterValidationException; +use ApiPlatform\Metadata\Operation; use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\ManagerRegistry; use Psr\Log\LoggerInterface; use RZ\Roadiz\CoreBundle\Entity\NodesSources; use RZ\Roadiz\CoreBundle\Entity\Translation; use RZ\Roadiz\CoreBundle\Preview\PreviewResolverInterface; -use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; final class LocaleFilter extends GeneratedEntityFilter { @@ -23,34 +24,23 @@ final class LocaleFilter extends GeneratedEntityFilter public function __construct( PreviewResolverInterface $previewResolver, ManagerRegistry $managerRegistry, - ?RequestStack $requestStack = null, - string $generatedEntityNamespacePattern = '#^App\\\GeneratedEntity\\\NS(?:[a-zA-Z]+)$#', LoggerInterface $logger = null, - array $properties = null + array $properties = null, + NameConverterInterface $nameConverter = null, + string $generatedEntityNamespacePattern = '#^App\\\GeneratedEntity\\\NS(?:[a-zA-Z]+)$#' ) { - parent::__construct($managerRegistry, $requestStack, $generatedEntityNamespacePattern, $logger, $properties); + parent::__construct($managerRegistry, $logger, $properties, $nameConverter, $generatedEntityNamespacePattern); $this->previewResolver = $previewResolver; } - - /** - * Passes a property through the filter. - * - * @param string $property - * @param mixed $value - * @param QueryBuilder $queryBuilder - * @param QueryNameGeneratorInterface $queryNameGenerator - * @param string $resourceClass - * @param string|null $operationName - * @throws \Exception - */ protected function filterProperty( string $property, - $value, + mixed $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, - string $operationName = null + ?Operation $operation = null, + array $context = [] ): void { if ($property !== self::PROPERTY) { return; diff --git a/src/Api/Filter/NotFilter.php b/src/Api/Filter/NotFilter.php index 22e37e2a..b1677bd0 100644 --- a/src/Api/Filter/NotFilter.php +++ b/src/Api/Filter/NotFilter.php @@ -4,33 +4,25 @@ namespace RZ\Roadiz\CoreBundle\Api\Filter; -use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\AbstractContextAwareFilter; -use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface; +use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; +use ApiPlatform\Doctrine\Orm\Filter\AbstractFilter; +use ApiPlatform\Metadata\Operation; +use Doctrine\ORM\Query\Expr\Join; use Doctrine\ORM\QueryBuilder; use Symfony\Component\String\Slugger\AsciiSlugger; -final class NotFilter extends AbstractContextAwareFilter +final class NotFilter extends AbstractFilter { public const PARAMETER = 'not'; - /** - * Passes a property through the filter. - * - * @param string $property - * @param mixed $value - * @param QueryBuilder $queryBuilder - * @param QueryNameGeneratorInterface $queryNameGenerator - * @param string $resourceClass - * @param string|null $operationName - * @throws \Exception - */ protected function filterProperty( string $property, - $value, + mixed $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, - string $operationName = null + ?Operation $operation = null, + array $context = [] ): void { if ($property !== self::PARAMETER || !\is_array($value)) { return; @@ -41,7 +33,14 @@ protected function filterProperty( $field = $property; if ($this->isPropertyNested($property, $resourceClass)) { - list($alias, $field) = $this->addJoinsForNestedProperty($property, $alias, $queryBuilder, $queryNameGenerator); + list($alias, $field) = $this->addJoinsForNestedProperty( + $property, + $alias, + $queryBuilder, + $queryNameGenerator, + $resourceClass, + Join::INNER_JOIN + ); } $placeholder = ':' . (new AsciiSlugger())->slug($alias . '_' . $field, '_')->toString(); diff --git a/src/Api/ListManager/SolrPaginator.php b/src/Api/ListManager/SolrPaginator.php index 99d467be..8eedf602 100644 --- a/src/Api/ListManager/SolrPaginator.php +++ b/src/Api/ListManager/SolrPaginator.php @@ -62,6 +62,10 @@ public function getItemsPerPage(): float public function getIterator(): \Traversable { $this->handleOnce(); - return new ArrayCollection($this->listManager->getEntities()); + $entities = $this->listManager->getEntities(); + if (\is_array($entities)) { + return new \ArrayIterator($entities); + } + return $entities->getIterator(); } } diff --git a/src/Api/ListManager/SolrSearchListManager.php b/src/Api/ListManager/SolrSearchListManager.php index 2185a575..d4c6e75f 100644 --- a/src/Api/ListManager/SolrSearchListManager.php +++ b/src/Api/ListManager/SolrSearchListManager.php @@ -15,6 +15,7 @@ final class SolrSearchListManager extends AbstractEntityListManager protected ?SearchResultsInterface $searchResults; private array $criteria; private bool $searchInTags; + private ?string $query = null; public function __construct( ?Request $request, @@ -34,49 +35,40 @@ public function handle(bool $disabled = false) throw new \InvalidArgumentException('Cannot handle a NULL request.'); } - $query = trim($this->request->query->get('search') ?? ''); + $this->handleRequestQuery($disabled); - if ( - $this->request->query->has('page') && - $this->request->query->get('page') > 1 - ) { - $this->setPage((int) $this->request->query->get('page')); - } else { - $this->setPage(1); - } - - if ( - $this->request->query->has('itemsPerPage') && - $this->request->query->get('itemsPerPage') > 0 - ) { - $this->setItemPerPage((int) $this->request->query->get('itemsPerPage')); + if (null === $this->query) { + throw new \InvalidArgumentException('Cannot handle a NULL query.'); } - /* * Query must be longer than 3 chars or Solr might crash * on highlighting fields. */ - if (\mb_strlen($query) > 3) { + if (\mb_strlen($this->query) > 3) { $this->searchResults = $this->searchHandler->searchWithHighlight( - $query, # Use ?q query parameter to search with + $this->query, # Use ?q query parameter to search with $this->criteria, # a simple criteria array to filter search results $this->getItemPerPage(), # result count $this->searchInTags, # Search in tags too, - 1, $this->getPage() ); } else { $this->searchResults = $this->searchHandler->search( - $query, # Use ?q query parameter to search with + $this->query, # Use ?q query parameter to search with $this->criteria, # a simple criteria array to filter search results $this->getItemPerPage(), # result count $this->searchInTags, # Search in tags too, - 2, $this->getPage() ); } } + protected function handleSearchParam(string $search): void + { + parent::handleSearchParam($search); + $this->query = trim($search); + } + /** * @inheritDoc */ @@ -91,7 +83,7 @@ public function getItemCount(): int /** * @inheritDoc */ - public function getEntities() + public function getEntities(): array { if (null !== $this->searchResults) { return $this->searchResults->getResultItems(); diff --git a/src/Api/Model/BlocksAwareWebResponseInterface.php b/src/Api/Model/BlocksAwareWebResponseInterface.php index e94917f7..bdf356fe 100644 --- a/src/Api/Model/BlocksAwareWebResponseInterface.php +++ b/src/Api/Model/BlocksAwareWebResponseInterface.php @@ -10,13 +10,13 @@ interface BlocksAwareWebResponseInterface extends WebResponseInterface { /** - * @return Collection|null + * @return WalkerInterface[]|null */ - public function getBlocks(): ?Collection; + public function getBlocks(): ?array; /** * @param Collection|null $blocks - * @return BlocksAwareWebResponseInterface + * @return $this */ - public function setBlocks(?Collection $blocks): BlocksAwareWebResponseInterface; + public function setBlocks(?Collection $blocks): self; } diff --git a/src/Api/Model/NodesSourcesHead.php b/src/Api/Model/NodesSourcesHead.php index ef94ded5..c0f52e2b 100644 --- a/src/Api/Model/NodesSourcesHead.php +++ b/src/Api/Model/NodesSourcesHead.php @@ -113,7 +113,6 @@ public function getMatomoSiteId(): ?string #[Serializer\Groups(["web_response", "nodes_sources_single", "walker"])] public function getSiteName(): ?string { - // site_name return $this->settingsBag->get('site_name', null) ?? null; } @@ -173,9 +172,6 @@ public function isNoIndex(): bool return false; } - /** - * @return string|null - */ #[Serializer\Groups(["web_response", "nodes_sources_single", "walker"])] public function getPolicyUrl(): ?string { @@ -199,63 +195,60 @@ public function getPolicyUrl(): ?string return null; } - /** - * @return string|null - */ #[Serializer\Groups(["web_response", "nodes_sources_single", "walker"])] public function getMainColor(): ?string { return $this->settingsBag->get('main_color', null) ?? null; } - /** - * @return string|null - */ #[Serializer\Groups(["web_response", "nodes_sources_single", "walker"])] public function getFacebookUrl(): ?string { return $this->settingsBag->get('facebook_url', null) ?? null; } - /** - * @return string|null - */ #[Serializer\Groups(["web_response", "nodes_sources_single", "walker"])] public function getInstagramUrl(): ?string { return $this->settingsBag->get('instagram_url', null) ?? null; } - /** - * @return string|null - */ #[Serializer\Groups(["web_response", "nodes_sources_single", "walker"])] public function getTwitterUrl(): ?string { return $this->settingsBag->get('twitter_url', null) ?? null; } - /** - * @return string|null - */ #[Serializer\Groups(["web_response", "nodes_sources_single", "walker"])] public function getYoutubeUrl(): ?string { return $this->settingsBag->get('youtube_url', null) ?? null; } - /** - * @return string|null - */ #[Serializer\Groups(["nodes_sources_single", "walker"])] public function getLinkedinUrl(): ?string { return $this->settingsBag->get('linkedin_url', null) ?? null; } - /** - * @return string|null - */ + #[Serializer\Groups(["nodes_sources_single", "walker"])] + public function getSpotifyUrl(): ?string + { + return $this->settingsBag->get('spotify_url', null) ?? null; + } + + #[Serializer\Groups(["nodes_sources_single", "walker"])] + public function getSoundcloudUrl(): ?string + { + return $this->settingsBag->get('soundcloud_url', null) ?? null; + } + + #[Serializer\Groups(["nodes_sources_single", "walker"])] + public function getTikTokUrl(): ?string + { + return $this->settingsBag->get('tiktok_url', null) ?? null; + } + #[Serializer\Groups(["web_response", "nodes_sources_single", "walker"])] public function getHomePageUrl(): ?string { @@ -268,9 +261,6 @@ public function getHomePageUrl(): ?string return null; } - /** - * @return DocumentInterface|null - */ #[Serializer\Groups(["web_response", "nodes_sources_single"])] public function getShareImage(): ?DocumentInterface { @@ -291,9 +281,6 @@ public function getShareImage(): ?DocumentInterface return $this->settingsBag->getDocument('share_image') ?? null; } - /** - * @return TranslationInterface - */ #[Serializer\Ignore()] public function getTranslation(): TranslationInterface { @@ -303,9 +290,6 @@ public function getTranslation(): TranslationInterface return $this->defaultTranslation; } - /** - * @return NodesSources|null - */ #[Serializer\Ignore()] public function getHomePage(): ?NodesSources { diff --git a/src/Api/Model/NodesSourcesHeadFactory.php b/src/Api/Model/NodesSourcesHeadFactory.php index d84fc468..9cae9098 100644 --- a/src/Api/Model/NodesSourcesHeadFactory.php +++ b/src/Api/Model/NodesSourcesHeadFactory.php @@ -13,27 +13,12 @@ final class NodesSourcesHeadFactory implements NodesSourcesHeadFactoryInterface { - private Settings $settingsBag; - private UrlGeneratorInterface $urlGenerator; - private NodeSourceApi $nodeSourceApi; - private HandlerFactoryInterface $handlerFactory; - - /** - * @param Settings $settingsBag - * @param UrlGeneratorInterface $urlGenerator - * @param NodeSourceApi $nodeSourceApi - * @param HandlerFactoryInterface $handlerFactory - */ public function __construct( - Settings $settingsBag, - UrlGeneratorInterface $urlGenerator, - NodeSourceApi $nodeSourceApi, - HandlerFactoryInterface $handlerFactory + private readonly Settings $settingsBag, + private readonly UrlGeneratorInterface $urlGenerator, + private readonly NodeSourceApi $nodeSourceApi, + private readonly HandlerFactoryInterface $handlerFactory ) { - $this->settingsBag = $settingsBag; - $this->urlGenerator = $urlGenerator; - $this->nodeSourceApi = $nodeSourceApi; - $this->handlerFactory = $handlerFactory; } public function createForNodeSource(NodesSources $nodesSources): NodesSourcesHeadInterface diff --git a/src/Api/Model/RealmsAwareWebResponseInterface.php b/src/Api/Model/RealmsAwareWebResponseInterface.php index 92e78e05..3962bfe0 100644 --- a/src/Api/Model/RealmsAwareWebResponseInterface.php +++ b/src/Api/Model/RealmsAwareWebResponseInterface.php @@ -15,9 +15,9 @@ public function getRealms(): ?array; /** * @param RealmInterface[]|null $realms - * @return RealmsAwareWebResponseInterface + * @return $this */ - public function setRealms(?array $realms): RealmsAwareWebResponseInterface; + public function setRealms(?array $realms): self; /** * @return bool @@ -26,7 +26,7 @@ public function isHidingBlocks(): bool; /** * @param bool $hidingBlocks - * @return RealmsAwareWebResponseInterface + * @return $this */ - public function setHidingBlocks(bool $hidingBlocks): RealmsAwareWebResponseInterface; + public function setHidingBlocks(bool $hidingBlocks): self; } diff --git a/src/Api/Model/WebResponse.php b/src/Api/Model/WebResponse.php index 2cf0b7e4..d8558dce 100644 --- a/src/Api/Model/WebResponse.php +++ b/src/Api/Model/WebResponse.php @@ -4,100 +4,7 @@ namespace RZ\Roadiz\CoreBundle\Api\Model; -use ApiPlatform\Core\Annotation\ApiProperty; -use Doctrine\Common\Collections\Collection; -use RZ\Roadiz\Core\AbstractEntities\PersistableInterface; -use RZ\Roadiz\CoreBundle\Api\Breadcrumbs\BreadcrumbsInterface; -use RZ\Roadiz\CoreBundle\Model\RealmInterface; -use RZ\TreeWalker\WalkerInterface; -use Symfony\Component\Serializer\Annotation as Serializer; - final class WebResponse implements WebResponseInterface, BlocksAwareWebResponseInterface, RealmsAwareWebResponseInterface { - #[ApiProperty(identifier: true)] - public ?string $path = null; - - #[Serializer\Groups(["web_response"])] - public ?PersistableInterface $item = null; - - #[Serializer\Groups(["web_response"])] - public ?BreadcrumbsInterface $breadcrumbs = null; - - #[Serializer\Groups(["web_response"])] - public ?NodesSourcesHeadInterface $head = null; - /** - * @var Collection|null - */ - #[Serializer\Groups(["web_response"])] - private ?Collection $blocks = null; - /** - * @var array|null - */ - #[Serializer\Groups(["web_response"])] - private ?array $realms = null; - - #[Serializer\Groups(["web_response"])] - private bool $hidingBlocks = false; - - /** - * @return PersistableInterface|null - */ - public function getItem(): ?PersistableInterface - { - return $this->item; - } - - /** - * @return Collection|null - */ - public function getBlocks(): ?Collection - { - return $this->blocks; - } - - /** - * @param Collection|null $blocks - * @return WebResponse - */ - public function setBlocks(?Collection $blocks): WebResponse - { - $this->blocks = $blocks; - return $this; - } - - /** - * @return RealmInterface[]|null - */ - public function getRealms(): ?array - { - return $this->realms; - } - - /** - * @param RealmInterface[]|null $realms - * @return WebResponse - */ - public function setRealms(?array $realms): WebResponse - { - $this->realms = $realms; - return $this; - } - - /** - * @return bool - */ - public function isHidingBlocks(): bool - { - return $this->hidingBlocks; - } - - /** - * @param bool $hidingBlocks - * @return WebResponse - */ - public function setHidingBlocks(bool $hidingBlocks): WebResponse - { - $this->hidingBlocks = $hidingBlocks; - return $this; - } + use WebResponseTrait; } diff --git a/src/Api/Model/WebResponseInterface.php b/src/Api/Model/WebResponseInterface.php index 589b7983..61ff1727 100644 --- a/src/Api/Model/WebResponseInterface.php +++ b/src/Api/Model/WebResponseInterface.php @@ -5,8 +5,26 @@ namespace RZ\Roadiz\CoreBundle\Api\Model; use RZ\Roadiz\Core\AbstractEntities\PersistableInterface; +use RZ\Roadiz\CoreBundle\Api\Breadcrumbs\BreadcrumbsInterface; +/** + * @template T of PersistableInterface + */ interface WebResponseInterface { + public function setHead(?NodesSourcesHeadInterface $head): self; + public function setBreadcrumbs(?BreadcrumbsInterface $breadcrumbs): self; + + /** + * @param T|null $item + * @return self + */ + public function setItem(?PersistableInterface $item): self; + public function setPath(?string $path): self; + /** + * @return T|null + */ public function getItem(): ?PersistableInterface; + public function getMaxAge(): ?int; + public function setMaxAge(?int $maxAge): self; } diff --git a/src/Api/Model/WebResponseTrait.php b/src/Api/Model/WebResponseTrait.php new file mode 100644 index 00000000..8acf455f --- /dev/null +++ b/src/Api/Model/WebResponseTrait.php @@ -0,0 +1,149 @@ +|null + */ + #[Serializer\Groups(["web_response"])] + private ?Collection $blocks = null; + /** + * @var array|null + */ + #[Serializer\Groups(["web_response"])] + private ?array $realms = null; + + #[Serializer\Groups(["web_response"])] + private bool $hidingBlocks = false; + + /** + * @var int|null WebResponse item maximum age in seconds + */ + #[Serializer\Groups(["web_response"])] + private ?int $maxAge = null; + + public function getMaxAge(): ?int + { + return $this->maxAge; + } + + public function setMaxAge(?int $maxAge): self + { + $this->maxAge = $maxAge; + return $this; + } + + public function setPath(?string $path): self + { + $this->path = $path; + return $this; + } + + public function setItem(?PersistableInterface $item): self + { + $this->item = $item; + return $this; + } + + /** + * @return PersistableInterface|null + */ + public function getItem(): ?PersistableInterface + { + return $this->item; + } + + /** + * @return WalkerInterface[]|null + */ + public function getBlocks(): ?array + { + return $this->blocks?->getValues(); + } + + /** + * @param Collection|null $blocks + * @return $this + */ + public function setBlocks(?Collection $blocks): self + { + $this->blocks = $blocks; + return $this; + } + + /** + * @return RealmInterface[]|null + */ + public function getRealms(): ?array + { + return $this->realms; + } + + /** + * @param RealmInterface[]|null $realms + * @return $this + */ + public function setRealms(?array $realms): self + { + $this->realms = $realms; + return $this; + } + + /** + * @return bool + */ + public function isHidingBlocks(): bool + { + return $this->hidingBlocks; + } + + /** + * @param bool $hidingBlocks + * @return $this + */ + public function setHidingBlocks(bool $hidingBlocks): self + { + $this->hidingBlocks = $hidingBlocks; + return $this; + } + + public function setBreadcrumbs(?BreadcrumbsInterface $breadcrumbs): self + { + $this->breadcrumbs = $breadcrumbs; + return $this; + } + + public function setHead(?NodesSourcesHeadInterface $head): self + { + $this->head = $head; + return $this; + } +} diff --git a/src/Api/OpenApi/WebResponseDecorator.php b/src/Api/OpenApi/WebResponseDecorator.php index da54e849..21131a1e 100644 --- a/src/Api/OpenApi/WebResponseDecorator.php +++ b/src/Api/OpenApi/WebResponseDecorator.php @@ -22,6 +22,10 @@ public function __invoke(array $context = []): OpenApi { $openApi = ($this->decorated)($context); $pathItem = $openApi->getPaths()->getPath('/api/web_response_by_path'); + if (null === $pathItem) { + return $openApi; + } + $operation = $pathItem->getGet(); $openApi->getPaths()->addPath('/api/web_response_by_path', $pathItem->withGet( diff --git a/src/Api/TreeWalker/Definition/DefinitionFactoryConfiguration.php b/src/Api/TreeWalker/Definition/DefinitionFactoryConfiguration.php new file mode 100644 index 00000000..a63ca9c1 --- /dev/null +++ b/src/Api/TreeWalker/Definition/DefinitionFactoryConfiguration.php @@ -0,0 +1,27 @@ +classname = $classname; + $this->onlyVisible = $onlyVisible; + $this->definitionFactory = $definitionFactory; + } +} diff --git a/src/Api/TreeWalker/Definition/DefinitionFactoryInterface.php b/src/Api/TreeWalker/Definition/DefinitionFactoryInterface.php new file mode 100644 index 00000000..85e75536 --- /dev/null +++ b/src/Api/TreeWalker/Definition/DefinitionFactoryInterface.php @@ -0,0 +1,12 @@ +getArrayCopy(); } + // @phpstan-ignore-next-line + return iterator_to_array($iterator); } return $children; } diff --git a/src/Api/TreeWalker/Definition/ReachableNodeSourceDefinition.php b/src/Api/TreeWalker/Definition/ReachableNodeSourceDefinition.php index 4ad2f99b..22fbc6ac 100644 --- a/src/Api/TreeWalker/Definition/ReachableNodeSourceDefinition.php +++ b/src/Api/TreeWalker/Definition/ReachableNodeSourceDefinition.php @@ -47,6 +47,8 @@ public function __invoke(NodesSources $source): array if ($iterator instanceof ArrayIterator) { return $iterator->getArrayCopy(); } + // @phpstan-ignore-next-line + return iterator_to_array($iterator); } return $children; } diff --git a/src/Api/TreeWalker/TreeWalkerGenerator.php b/src/Api/TreeWalker/TreeWalkerGenerator.php index 1e357445..7a8bff01 100644 --- a/src/Api/TreeWalker/TreeWalkerGenerator.php +++ b/src/Api/TreeWalker/TreeWalkerGenerator.php @@ -6,9 +6,12 @@ use Psr\Cache\CacheItemPoolInterface; use RZ\Roadiz\Core\AbstractEntities\TranslationInterface; +use RZ\Roadiz\CoreBundle\Api\TreeWalker\Definition\DefinitionFactoryConfiguration; +use RZ\Roadiz\CoreBundle\Api\TreeWalker\Definition\DefinitionFactoryInterface; use RZ\Roadiz\CoreBundle\Bag\NodeTypes; use RZ\Roadiz\CoreBundle\Entity\NodesSources; use RZ\Roadiz\CoreBundle\EntityApi\NodeSourceApi; +use RZ\TreeWalker\AbstractWalker; use RZ\TreeWalker\WalkerContextInterface; use RZ\TreeWalker\WalkerInterface; use Symfony\Component\String\UnicodeString; @@ -20,6 +23,11 @@ final class TreeWalkerGenerator private WalkerContextInterface $walkerContext; private CacheItemPoolInterface $cacheItemPool; + /** + * @var array + */ + private array $walkerDefinitionFactories = []; + public function __construct( NodeSourceApi $nodeSourceApi, NodeTypes $nodeTypesBag, @@ -34,7 +42,7 @@ public function __construct( /** * @param string $nodeType - * @param class-string $walkerClass + * @param class-string $walkerClass * @param TranslationInterface $translation * @param int $maxLevel * @return array @@ -54,14 +62,14 @@ public function getTreeWalkersForTypeAtRoot( ]); foreach ($roots as $root) { - $walkerName = (new UnicodeString($root->getNode()?->getNodeName() . ' walker')) + $walkerName = (new UnicodeString($root->getNode()->getNodeName() . ' walker')) ->trim() ->camel() ->toString(); - $walkers[$walkerName] = call_user_func( - [$walkerClass, 'build'], + $walkers[$walkerName] = $this->buildForRoot( $root, + $walkerClass, $this->walkerContext, $maxLevel, $this->cacheItemPool @@ -70,4 +78,62 @@ public function getTreeWalkersForTypeAtRoot( return $walkers; } + + /** + * @param object $root + * @param class-string $walkerClass + * @param WalkerContextInterface $walkerContext + * @param int $maxLevel + * @param CacheItemPoolInterface $cacheItemPool + * @return WalkerInterface + */ + public function buildForRoot( + object $root, + string $walkerClass, + WalkerContextInterface $walkerContext, + int $maxLevel, + CacheItemPoolInterface $cacheItemPool + ): WalkerInterface { + /** @var callable $callable */ + $callable = [$walkerClass, 'build']; + $walker = call_user_func( + $callable, + $root, + $walkerContext, + $maxLevel, + $cacheItemPool + ); + + foreach ($this->walkerDefinitionFactories as $definitionFactoryConfiguration) { + $walker->addDefinition( + $definitionFactoryConfiguration->classname, + $definitionFactoryConfiguration->definitionFactory->create( + $this->walkerContext, + $definitionFactoryConfiguration->onlyVisible + ) + ); + } + return $walker; + } + + /** + * Inject definition from factories registered in the container + * using `roadiz_core.tree_walker_definition_factory` tag. + * + * @param class-string $classname + * @param DefinitionFactoryInterface $definitionFactory + * @param bool $onlyVisible + * @return void + */ + public function addDefinitionFactoryConfiguration( + string $classname, + DefinitionFactoryInterface $definitionFactory, + bool $onlyVisible + ): void { + $this->walkerDefinitionFactories[$classname] = new DefinitionFactoryConfiguration( + $classname, + $definitionFactory, + $onlyVisible + ); + } } diff --git a/src/Bag/NodeTypes.php b/src/Bag/NodeTypes.php index 44147c4a..fbaf9dd5 100644 --- a/src/Bag/NodeTypes.php +++ b/src/Bag/NodeTypes.php @@ -10,23 +10,15 @@ use RZ\Roadiz\CoreBundle\Entity\NodeType; use RZ\Roadiz\CoreBundle\Repository\NodeTypeRepository; -class NodeTypes extends LazyParameterBag implements NodeTypeResolverInterface +final class NodeTypes extends LazyParameterBag implements NodeTypeResolverInterface { private ?NodeTypeRepository $repository = null; - private ManagerRegistry $managerRegistry; - /** - * @param ManagerRegistry $managerRegistry - */ - public function __construct(ManagerRegistry $managerRegistry) + public function __construct(private readonly ManagerRegistry $managerRegistry) { parent::__construct(); - $this->managerRegistry = $managerRegistry; } - /** - * @return NodeTypeRepository - */ public function getRepository(): NodeTypeRepository { if (null === $this->repository) { diff --git a/src/Bag/Roles.php b/src/Bag/Roles.php index 4d13624c..e98d7e14 100644 --- a/src/Bag/Roles.php +++ b/src/Bag/Roles.php @@ -9,23 +9,15 @@ use RZ\Roadiz\CoreBundle\Entity\Role; use RZ\Roadiz\CoreBundle\Repository\RoleRepository; -class Roles extends LazyParameterBag +final class Roles extends LazyParameterBag { - private ManagerRegistry $managerRegistry; private ?RoleRepository $repository = null; - /** - * @param ManagerRegistry $managerRegistry; - */ - public function __construct(ManagerRegistry $managerRegistry) + public function __construct(private readonly ManagerRegistry $managerRegistry) { parent::__construct(); - $this->managerRegistry = $managerRegistry; } - /** - * @return RoleRepository - */ public function getRepository(): RoleRepository { if (null === $this->repository) { @@ -57,14 +49,15 @@ protected function populateParameters(): void * * @return Role */ - public function get($key, $default = null): Role + public function get(string $key, $default = null): Role { $role = parent::get($key, $default); if (null === $role) { $role = new Role($key); - $this->managerRegistry->getManagerForClass(Role::class)->persist($role); - $this->managerRegistry->getManagerForClass(Role::class)->flush(); + $roleManager = $this->managerRegistry->getManagerForClass(Role::class); + $roleManager->persist($role); + $roleManager->flush(); } return $role; diff --git a/src/Bag/Settings.php b/src/Bag/Settings.php index 128d9b89..4760721b 100644 --- a/src/Bag/Settings.php +++ b/src/Bag/Settings.php @@ -9,24 +9,19 @@ use RZ\Roadiz\CoreBundle\Entity\Document; use RZ\Roadiz\CoreBundle\Entity\Setting; use RZ\Roadiz\CoreBundle\Repository\SettingRepository; +use Symfony\Component\Stopwatch\Stopwatch; class Settings extends LazyParameterBag { - private ManagerRegistry $managerRegistry; private ?SettingRepository $repository = null; - /** - * @param ManagerRegistry $managerRegistry - */ - public function __construct(ManagerRegistry $managerRegistry) - { + public function __construct( + private readonly ManagerRegistry $managerRegistry, + private readonly Stopwatch $stopwatch + ) { parent::__construct(); - $this->managerRegistry = $managerRegistry; } - /** - * @return SettingRepository - */ public function getRepository(): SettingRepository { if (null === $this->repository) { @@ -37,6 +32,7 @@ public function getRepository(): SettingRepository protected function populateParameters(): void { + $this->stopwatch->start('settings'); try { $settings = $this->getRepository()->findAll(); $this->parameters = []; @@ -48,14 +44,15 @@ protected function populateParameters(): void $this->parameters = []; } $this->ready = true; + $this->stopwatch->stop('settings'); } /** * @param string $key * @param mixed $default - * @return bool|mixed + * @return mixed */ - public function get($key, $default = false) + public function get(string $key, $default = false): mixed { return parent::get($key, $default); } @@ -66,7 +63,7 @@ public function get($key, $default = false) * @param string $key * @return Document|null */ - public function getDocument($key): ?Document + public function getDocument(string $key): ?Document { try { $id = $this->getInt($key); diff --git a/src/Cache/Clearer/FileClearer.php b/src/Cache/Clearer/FileClearer.php index 0cb90568..955e348f 100644 --- a/src/Cache/Clearer/FileClearer.php +++ b/src/Cache/Clearer/FileClearer.php @@ -7,11 +7,9 @@ abstract class FileClearer implements ClearerInterface { protected ?string $output = null; - protected string $cacheDir; - public function __construct(string $cacheDir) + public function __construct(protected readonly string $cacheDir) { - $this->cacheDir = $cacheDir; } public function clear(): bool diff --git a/src/Cache/Clearer/NodesSourcesUrlsCacheClearer.php b/src/Cache/Clearer/NodesSourcesUrlsCacheClearer.php index f59f919b..a7018fe0 100644 --- a/src/Cache/Clearer/NodesSourcesUrlsCacheClearer.php +++ b/src/Cache/Clearer/NodesSourcesUrlsCacheClearer.php @@ -8,12 +8,9 @@ final class NodesSourcesUrlsCacheClearer extends FileClearer { - private CacheItemPoolInterface $cacheProvider; - - public function __construct(CacheItemPoolInterface $cacheProvider) + public function __construct(private readonly CacheItemPoolInterface $cacheProvider) { parent::__construct(''); - $this->cacheProvider = $cacheProvider; } public function clear(): bool diff --git a/src/Cache/CloudflareProxyCache.php b/src/Cache/CloudflareProxyCache.php index 15e63174..93485e54 100644 --- a/src/Cache/CloudflareProxyCache.php +++ b/src/Cache/CloudflareProxyCache.php @@ -6,32 +6,15 @@ final class CloudflareProxyCache { - protected string $name; - protected string $zone; - protected string $version; - protected string $bearer; - protected string $email; - protected string $key; - protected int $timeout; - - /** - * @param string $name - * @param string $zone - * @param string $version - * @param string $bearer - * @param string $email - * @param string $key - * @param int $timeout - */ - public function __construct(string $name, string $zone, string $version, string $bearer, string $email, string $key, int $timeout) - { - $this->name = $name; - $this->zone = $zone; - $this->version = $version; - $this->bearer = $bearer; - $this->email = $email; - $this->key = $key; - $this->timeout = $timeout; + public function __construct( + private readonly string $name, + private readonly string $zone, + private readonly string $version, + private readonly string $bearer, + private readonly string $email, + private readonly string $key, + private readonly int $timeout + ) { } /** diff --git a/src/Cache/ReverseProxyCache.php b/src/Cache/ReverseProxyCache.php index 80900a63..8aa36611 100644 --- a/src/Cache/ReverseProxyCache.php +++ b/src/Cache/ReverseProxyCache.php @@ -6,23 +6,12 @@ final class ReverseProxyCache { - protected string $name; - protected string $host; - protected string $domainName; - protected int $timeout; - - /** - * @param string $name - * @param string $host - * @param string $domainName - * @param int $timeout - */ - public function __construct(string $name, string $host, string $domainName, int $timeout) - { - $this->name = $name; - $this->host = $host; - $this->domainName = $domainName; - $this->timeout = $timeout; + public function __construct( + private readonly string $name, + private readonly string $host, + private readonly string $domainName, + private readonly int $timeout + ) { } /** @@ -48,4 +37,9 @@ public function getDomainName(): string { return $this->domainName; } + + public function getTimeout(): int + { + return $this->timeout; + } } diff --git a/src/Cache/ReverseProxyCacheLocator.php b/src/Cache/ReverseProxyCacheLocator.php index b6a4a3f6..b3f4b963 100644 --- a/src/Cache/ReverseProxyCacheLocator.php +++ b/src/Cache/ReverseProxyCacheLocator.php @@ -6,20 +6,14 @@ final class ReverseProxyCacheLocator { - /** - * @var ReverseProxyCache[] - */ - private array $frontends; - private ?CloudflareProxyCache $cloudflareProxyCache; - /** * @param ReverseProxyCache[] $frontends * @param CloudflareProxyCache|null $cloudflareProxyCache */ - public function __construct(array $frontends, ?CloudflareProxyCache $cloudflareProxyCache = null) - { - $this->frontends = $frontends; - $this->cloudflareProxyCache = $cloudflareProxyCache; + public function __construct( + private readonly array $frontends, + private readonly ?CloudflareProxyCache $cloudflareProxyCache = null + ) { } /** diff --git a/src/Console/AppInstallCommand.php b/src/Console/AppInstallCommand.php new file mode 100644 index 00000000..e1023ba7 --- /dev/null +++ b/src/Console/AppInstallCommand.php @@ -0,0 +1,168 @@ +setName('app:install') + ->setDescription('Install application fixtures (node-types, settings, roles) from config.yml') + ->addOption( + 'dry-run', + 'd', + InputOption::VALUE_NONE, + 'Do nothing, only print information.' + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + if ($input->getOption('dry-run')) { + $this->dryRun = true; + } + $this->io = new SymfonyStyle($input, $output); + + /* + * Test if Classname is not a valid yaml file before using Theme + */ + $configPath = $this->projectDir . '/src/Resources/config.yml'; + $realConfigPath = realpath($configPath); + if (false !== $realConfigPath && file_exists($realConfigPath)) { + $this->io->note('Install assets directly from file: ' . $realConfigPath); + $themeConfigPath = $realConfigPath; + } else { + $this->io->error($configPath . ' configuration file is not readable.'); + return 1; + } + + $this->importAppData($themeConfigPath); + return 0; + } + + protected function importAppData(string $themeConfigPath): void + { + $data = $this->getAppConfig($themeConfigPath); + + if (isset($data["importFiles"])) { + if (isset($data["importFiles"]['groups'])) { + foreach ($data["importFiles"]['groups'] as $filename) { + $this->importFile($filename, $this->groupsImporter); + } + } + if (isset($data["importFiles"]['roles'])) { + foreach ($data["importFiles"]['roles'] as $filename) { + $this->importFile($filename, $this->rolesImporter); + } + } + if (isset($data["importFiles"]['settings'])) { + foreach ($data["importFiles"]['settings'] as $filename) { + $this->importFile($filename, $this->settingsImporter); + } + } + if (isset($data["importFiles"]['nodetypes'])) { + foreach ($data["importFiles"]['nodetypes'] as $filename) { + $this->importFile($filename, $this->nodeTypesImporter); + } + } + if (isset($data["importFiles"]['tags'])) { + foreach ($data["importFiles"]['tags'] as $filename) { + $this->importFile($filename, $this->tagsImporter); + } + } + if (isset($data["importFiles"]['attributes'])) { + foreach ($data["importFiles"]['attributes'] as $filename) { + $this->importFile($filename, $this->attributeImporter); + } + } + } else { + $this->io->warning('Config file "' . $themeConfigPath . '" has no data to import.'); + } + } + + /** + * @param string $filename + * @param EntityImporterInterface $importer + */ + protected function importFile(string $filename, EntityImporterInterface $importer): void + { + if (false !== $realFilename = realpath($filename)) { + $file = new File($realFilename); + } else { + throw new \RuntimeException($filename . ' is not a valid file'); + } + if (!$this->dryRun) { + try { + if (false === $fileContent = file_get_contents($file->getPathname())) { + throw new \RuntimeException($file->getPathname() . ' file is not readable'); + } + $importer->import($fileContent); + $this->managerRegistry->getManager()->flush(); + $this->io->writeln( + '* ' . $file->getPathname() . ' file has been imported.' + ); + return; + } catch (EntityAlreadyExistsException $e) { + $this->io->writeln( + '* ' . $file->getPathname() . '' . + ' has NOT been imported (' . $e->getMessage() . ').' + ); + } + } + $this->io->writeln( + '* ' . $file->getPathname() . ' file has been imported.' + ); + } + + /** + * @param string $appConfigPath + * @return array + */ + protected function getAppConfig(string $appConfigPath): array + { + if (false === $fileContent = file_get_contents($appConfigPath)) { + throw new \RuntimeException($appConfigPath . ' file is not readable'); + } + $data = Yaml::parse($fileContent); + if (!\is_array($data)) { + throw new \RuntimeException($appConfigPath . ' file is not a valid YAML file'); + } + return $data; + } +} diff --git a/src/Console/AppMigrateCommand.php b/src/Console/AppMigrateCommand.php new file mode 100644 index 00000000..e8949486 --- /dev/null +++ b/src/Console/AppMigrateCommand.php @@ -0,0 +1,126 @@ +setName('app:migrate') + ->setDescription('Perform app:install and generate NS entities classes and Doctrine migrations.') + ->addOption( + 'dry-run', + 'd', + InputOption::VALUE_NONE, + 'Do nothing, only print information.' + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + + $question = new ConfirmationQuestion( + 'Are you sure to migrate against app config.yml file? This will generate new Doctrine Migrations and execute them. If you just want to import node-types run `bin/console app:install` instead', + !$input->isInteractive() + ); + if ($io->askQuestion($question) === false) { + $io->note('Nothing was done…'); + return 0; + } + + if ($input->getOption('dry-run')) { + $this->runCommand( + 'app:install', + '--dry-run', + null, + $input->isInteractive(), + $output->isQuiet(), + ); + } else { + $this->runCommand( + 'app:install', + '', + null, + $input->isInteractive(), + $output->isQuiet() + ) === 0 ? $io->success('app:install') : $io->error('app:install'); + + $this->runCommand( + 'generate:nsentities', + '', + null, + $input->isInteractive(), + $output->isQuiet() + ) === 0 ? $io->success('generate:nsentities') : $io->error('generate:nsentities'); + + $this->schemaUpdater->updateNodeTypesSchema(); + $this->schemaUpdater->updateSchema(); + $io->success('doctrine-migrations'); + + $this->runCommand( + 'doctrine:cache:clear-metadata', + '', + null, + false, + true + ) === 0 ? $io->success('doctrine:cache:clear-metadata') : $io->error('doctrine:cache:clear-metadata'); + + $this->runCommand( + 'cache:clear', + '', + null, + false, + true + ) === 0 ? $io->success('cache:clear') : $io->error('cache:clear'); + + $this->runCommand( + 'cache:pool:clear', + 'cache.global_clearer', + null, + false, + true + ) === 0 ? $io->success('cache:pool:clear') : $io->error('cache:pool:clear'); + } + return 0; + } + + protected function runCommand( + string $command, + string $args = '', + ?string $environment = null, + bool $interactive = true, + bool $quiet = false + ): int { + $args .= $interactive ? '' : ' --no-interaction '; + $args .= $quiet ? ' --quiet ' : ' -v '; + $args .= is_string($environment) ? (' --env ' . $environment) : ''; + + $process = Process::fromShellCommandline( + 'php bin/console ' . $command . ' ' . $args + ); + $process->setWorkingDirectory($this->projectDir); + $process->setTty($interactive); + $process->run(); + return $process->wait(); + } +} diff --git a/src/Console/CleanLoginAttemptCommand.php b/src/Console/CleanLoginAttemptCommand.php deleted file mode 100644 index 758ac145..00000000 --- a/src/Console/CleanLoginAttemptCommand.php +++ /dev/null @@ -1,43 +0,0 @@ -managerRegistry = $managerRegistry; - } - - protected function configure(): void - { - $this->setName('login-attempts:clean') - ->setDescription('Clean all login attempts older than 1 day'); - } - - protected function execute(InputInterface $input, OutputInterface $output): int - { - $io = new SymfonyStyle($input, $output); - - $this->managerRegistry->getRepository(LoginAttempt::class)->cleanLoginAttempts(); - - $io->success('All login attempts older than 1 day were deleted.'); - - return 0; - } -} diff --git a/src/Console/CustomFormAnswerPurgeCommand.php b/src/Console/CustomFormAnswerPurgeCommand.php index 5363c503..07270a01 100644 --- a/src/Console/CustomFormAnswerPurgeCommand.php +++ b/src/Console/CustomFormAnswerPurgeCommand.php @@ -19,20 +19,13 @@ final class CustomFormAnswerPurgeCommand extends Command { - private ManagerRegistry $managerRegistry; - private EventDispatcherInterface $eventDispatcher; - private LoggerInterface $logger; - public function __construct( - ManagerRegistry $managerRegistry, - EventDispatcherInterface $eventDispatcher, - LoggerInterface $logger, + private readonly ManagerRegistry $managerRegistry, + private readonly EventDispatcherInterface $eventDispatcher, + private readonly LoggerInterface $logger, string $name = null ) { parent::__construct($name); - $this->managerRegistry = $managerRegistry; - $this->eventDispatcher = $eventDispatcher; - $this->logger = $logger; } protected function configure(): void diff --git a/src/Console/DecodePrivateKeyCommand.php b/src/Console/DecodePrivateKeyCommand.php deleted file mode 100644 index e213f591..00000000 --- a/src/Console/DecodePrivateKeyCommand.php +++ /dev/null @@ -1,49 +0,0 @@ -keyChain = $keyChain; - $this->uniqueKeyEncoderFactory = $uniqueKeyEncoderFactory; - } - - protected function configure(): void - { - $this->setName('crypto:private-key:decode') - ->addArgument('key-name', InputArgument::REQUIRED) - ->addArgument('data', InputArgument::REQUIRED) - ; - } - - protected function execute(InputInterface $input, OutputInterface $output): int - { - $io = new SymfonyStyle($input, $output); - - $keyName = $input->getArgument('key-name'); - $encoder = $this->uniqueKeyEncoderFactory->getEncoder($keyName); - $encoded = $encoder->decode($input->getArgument('data')); - - $io->note($encoded->getString()); - return 0; - } -} diff --git a/src/Console/EncodePrivateKeyCommand.php b/src/Console/EncodePrivateKeyCommand.php deleted file mode 100644 index 763b2ffa..00000000 --- a/src/Console/EncodePrivateKeyCommand.php +++ /dev/null @@ -1,50 +0,0 @@ -keyChain = $keyChain; - $this->uniqueKeyEncoderFactory = $uniqueKeyEncoderFactory; - } - - protected function configure(): void - { - $this->setName('crypto:private-key:encode') - ->addArgument('key-name', InputArgument::REQUIRED) - ->addArgument('data', InputArgument::REQUIRED) - ; - } - - protected function execute(InputInterface $input, OutputInterface $output): int - { - $io = new SymfonyStyle($input, $output); - - $keyName = $input->getArgument('key-name'); - $encoder = $this->uniqueKeyEncoderFactory->getEncoder($keyName); - $encoded = $encoder->encode(new HiddenString($input->getArgument('data'))); - - $io->note($encoded); - return 0; - } -} diff --git a/src/Console/FilesCommandTrait.php b/src/Console/FilesCommandTrait.php index e215fe70..15d73514 100644 --- a/src/Console/FilesCommandTrait.php +++ b/src/Console/FilesCommandTrait.php @@ -9,7 +9,7 @@ trait FilesCommandTrait /** * @return string */ - protected function getPublicFolderName() + protected function getPublicFolderName(): string { return '/exported_public'; } @@ -17,7 +17,7 @@ protected function getPublicFolderName() /** * @return string */ - protected function getPrivateFolderName() + protected function getPrivateFolderName(): string { return '/exported_private'; } @@ -25,7 +25,7 @@ protected function getPrivateFolderName() /** * @return string */ - protected function getFontsFolderName() + protected function getFontsFolderName(): string { return '/exported_fonts'; } diff --git a/src/Console/FilesExportCommand.php b/src/Console/FilesExportCommand.php index 29fac777..adf93e2d 100644 --- a/src/Console/FilesExportCommand.php +++ b/src/Console/FilesExportCommand.php @@ -14,20 +14,17 @@ use Symfony\Component\String\Slugger\AsciiSlugger; use ZipArchive; -class FilesExportCommand extends Command +final class FilesExportCommand extends Command { use FilesCommandTrait; - protected FileAwareInterface $fileAware; - protected string $exportDir; - protected string $appNamespace; - - public function __construct(FileAwareInterface $fileAware, string $exportDir, string $appNamespace) - { - parent::__construct(); - $this->fileAware = $fileAware; - $this->exportDir = $exportDir; - $this->appNamespace = $appNamespace; + public function __construct( + private readonly FileAwareInterface $fileAware, + private readonly string $exportDir, + private readonly string $appNamespace, + ?string $name = null + ) { + parent::__construct($name); } protected function configure(): void diff --git a/src/Console/FilesImportCommand.php b/src/Console/FilesImportCommand.php index 283cadc3..4b910575 100644 --- a/src/Console/FilesImportCommand.php +++ b/src/Console/FilesImportCommand.php @@ -15,20 +15,16 @@ use Symfony\Component\String\Slugger\AsciiSlugger; use ZipArchive; -class FilesImportCommand extends Command +final class FilesImportCommand extends Command { use FilesCommandTrait; - protected FileAwareInterface $fileAware; - protected string $exportDir; - protected string $appNamespace; - - public function __construct(FileAwareInterface $fileAware, string $exportDir, string $appNamespace) - { - parent::__construct(); - $this->fileAware = $fileAware; - $this->exportDir = $exportDir; - $this->appNamespace = $appNamespace; + public function __construct( + private readonly FileAwareInterface $fileAware, + private readonly string $appNamespace, + ?string $name = null + ) { + parent::__construct($name); } protected function configure(): void @@ -57,6 +53,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int $appNamespace = (new AsciiSlugger())->slug($this->appNamespace, '_'); $tempDir = tempnam(sys_get_temp_dir(), $appNamespace . '_files'); + if (false === $tempDir) { + throw new \RuntimeException('Cannot create temporary directory.'); + } if (file_exists($tempDir)) { unlink($tempDir); } diff --git a/src/Console/GenerateApiResourceCommand.php b/src/Console/GenerateApiResourceCommand.php index 7bfda603..87ae30e3 100644 --- a/src/Console/GenerateApiResourceCommand.php +++ b/src/Console/GenerateApiResourceCommand.php @@ -12,18 +12,14 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; -class GenerateApiResourceCommand extends Command +final class GenerateApiResourceCommand extends Command { - protected ManagerRegistry $managerRegistry; - protected ApiResourceGenerator $apiResourceGenerator; - public function __construct( - ManagerRegistry $managerRegistry, - ApiResourceGenerator $apiResourceGenerator + private readonly ManagerRegistry $managerRegistry, + private readonly ApiResourceGenerator $apiResourceGenerator, + ?string $name = null ) { - parent::__construct(); - $this->managerRegistry = $managerRegistry; - $this->apiResourceGenerator = $apiResourceGenerator; + parent::__construct($name); } protected function configure(): void diff --git a/src/Console/GenerateNodeSourceEntitiesCommand.php b/src/Console/GenerateNodeSourceEntitiesCommand.php index 1a630456..04c3b8dc 100644 --- a/src/Console/GenerateNodeSourceEntitiesCommand.php +++ b/src/Console/GenerateNodeSourceEntitiesCommand.php @@ -5,6 +5,8 @@ namespace RZ\Roadiz\CoreBundle\Console; use Doctrine\Persistence\ManagerRegistry; +use Psr\Container\ContainerExceptionInterface; +use Psr\Container\NotFoundExceptionInterface; use RZ\Roadiz\CoreBundle\Entity\NodeType; use RZ\Roadiz\CoreBundle\EntityHandler\HandlerFactory; use RZ\Roadiz\CoreBundle\EntityHandler\NodeTypeHandler; @@ -13,23 +15,14 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; -/** - * Command line utils for managing node-types from terminal. - */ -class GenerateNodeSourceEntitiesCommand extends Command +final class GenerateNodeSourceEntitiesCommand extends Command { - protected ManagerRegistry $managerRegistry; - protected HandlerFactory $handlerFactory; - - /** - * @param ManagerRegistry $managerRegistry - * @param HandlerFactory $handlerFactory - */ - public function __construct(ManagerRegistry $managerRegistry, HandlerFactory $handlerFactory) - { - parent::__construct(); - $this->managerRegistry = $managerRegistry; - $this->handlerFactory = $handlerFactory; + public function __construct( + private readonly ManagerRegistry $managerRegistry, + private readonly HandlerFactory $handlerFactory, + ?string $name = null + ) { + parent::__construct($name); } protected function configure(): void @@ -38,6 +31,10 @@ protected function configure(): void ->setDescription('Generate node-sources entities PHP classes.'); } + /** + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); diff --git a/src/Console/GeneratePrivateKeyCommand.php b/src/Console/GeneratePrivateKeyCommand.php deleted file mode 100644 index 3f46c280..00000000 --- a/src/Console/GeneratePrivateKeyCommand.php +++ /dev/null @@ -1,47 +0,0 @@ -keyChain = $keyChain; - $this->privateKeyName = $privateKeyName; - } - - protected function configure(): void - { - $this->setName('crypto:private-key:generate') - ->setDescription('Generate a default private key to encode data in your database.') - ; - } - - protected function execute(InputInterface $input, OutputInterface $output): int - { - $io = new SymfonyStyle($input, $output); - - $this->keyChain->generate($this->privateKeyName); - $io->success(sprintf('Private key has been generated: %s', $this->privateKeyName)); - return 0; - } -} diff --git a/src/Console/GetCronLastExecDateCommand.php b/src/Console/GetCronLastExecDateCommand.php new file mode 100644 index 00000000..e090c16d --- /dev/null +++ b/src/Console/GetCronLastExecDateCommand.php @@ -0,0 +1,43 @@ +settingRepository->findOneByName('cron_last_exec_date'); + if (!($setting instanceof Setting)) { + $io->warning('Last execution date of cron job has not been persisted yet.'); + return Command::FAILURE; + } + + $io->success(sprintf( + 'Last execution date of cron job is %s.', + $setting->getRawValue() + )); + + return Command::SUCCESS; + } +} diff --git a/src/Console/InstallCommand.php b/src/Console/InstallCommand.php index 0e21d59e..913de1ca 100644 --- a/src/Console/InstallCommand.php +++ b/src/Console/InstallCommand.php @@ -18,33 +18,16 @@ use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Yaml\Yaml; -/** - * Command line utils for installing RZ-CMS v3 from terminal. - */ -class InstallCommand extends Command +final class InstallCommand extends Command { - protected ManagerRegistry $managerRegistry; - protected RolesImporter $rolesImporter; - protected GroupsImporter $groupsImporter; - protected SettingsImporter $settingsImporter; - - /** - * @param ManagerRegistry $managerRegistry - * @param RolesImporter $rolesImporter - * @param GroupsImporter $groupsImporter - * @param SettingsImporter $settingsImporter - */ public function __construct( - ManagerRegistry $managerRegistry, - RolesImporter $rolesImporter, - GroupsImporter $groupsImporter, - SettingsImporter $settingsImporter + private readonly ManagerRegistry $managerRegistry, + private readonly RolesImporter $rolesImporter, + private readonly GroupsImporter $groupsImporter, + private readonly SettingsImporter $settingsImporter, + ?string $name = null ) { - parent::__construct(); - $this->managerRegistry = $managerRegistry; - $this->rolesImporter = $rolesImporter; - $this->groupsImporter = $groupsImporter; - $this->settingsImporter = $settingsImporter; + parent::__construct($name); } protected function configure(): void @@ -70,26 +53,48 @@ protected function execute(InputInterface $input, OutputInterface $output): int $io->askQuestion($question) ) { $fixturesRoot = dirname(__DIR__) . '/../config'; - $data = Yaml::parse(file_get_contents($fixturesRoot . "/fixtures.yaml")); + $fixtureFile = file_get_contents($fixturesRoot . "/fixtures.yaml"); + + if (false === $fixtureFile) { + $io->error('No fixtures.yaml file found in ' . $fixturesRoot); + return 1; + } + + $data = Yaml::parse($fixtureFile); if (isset($data["importFiles"]['roles'])) { foreach ($data["importFiles"]['roles'] as $filename) { $filePath = $fixturesRoot . "/" . $filename; - $this->rolesImporter->import(file_get_contents($filePath)); + $fileContents = file_get_contents($filePath); + if (false === $fileContents) { + $io->error('No file found in ' . $filePath); + return 1; + } + $this->rolesImporter->import($fileContents); $io->success('Theme file “' . $filePath . '” has been imported.'); } } if (isset($data["importFiles"]['groups'])) { foreach ($data["importFiles"]['groups'] as $filename) { $filePath = $fixturesRoot . "/" . $filename; - $this->groupsImporter->import(file_get_contents($filePath)); + $fileContents = file_get_contents($filePath); + if (false === $fileContents) { + $io->error('No file found in ' . $filePath); + return 1; + } + $this->groupsImporter->import($fileContents); $io->success('Theme file “' . $filePath . '” has been imported.'); } } if (isset($data["importFiles"]['settings'])) { foreach ($data["importFiles"]['settings'] as $filename) { $filePath = $fixturesRoot . "/" . $filename; - $this->settingsImporter->import(file_get_contents($filePath)); + $fileContents = file_get_contents($filePath); + if (false === $fileContents) { + $io->error('No file found in ' . $filePath); + return 1; + } + $this->settingsImporter->import($fileContents); $io->success('Theme files “' . $filePath . '” has been imported.'); } } diff --git a/src/Console/LogsCleanupCommand.php b/src/Console/LogsCleanupCommand.php index 6e00af9a..56fd662a 100644 --- a/src/Console/LogsCleanupCommand.php +++ b/src/Console/LogsCleanupCommand.php @@ -6,25 +6,22 @@ use Doctrine\ORM\NoResultException; use Doctrine\Persistence\ManagerRegistry; -use RZ\Roadiz\CoreBundle\Entity\Log; +use RZ\Roadiz\CoreBundle\Logger\Entity\Log; use RZ\Roadiz\CoreBundle\Repository\LogRepository; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; -class LogsCleanupCommand extends Command +final class LogsCleanupCommand extends Command { - protected ManagerRegistry $managerRegistry; - - /** - * @param ManagerRegistry $managerRegistry - */ - public function __construct(ManagerRegistry $managerRegistry) - { - parent::__construct(); - $this->managerRegistry = $managerRegistry; + public function __construct( + private readonly ManagerRegistry $managerRegistry, + ?string $name = null + ) { + parent::__construct($name); } protected function configure(): void @@ -47,7 +44,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int if (\is_string($input->getOption('since'))) { $since = '-' . $input->getOption('since'); } - $now->add(\DateInterval::createFromDateString($since)); + $interval = \DateInterval::createFromDateString($since); + if (false === $interval) { + throw new InvalidArgumentException('Invalid since option format.'); + } + $now->add($interval); $io = new SymfonyStyle($input, $output); /** @var LogRepository $logRepository */ diff --git a/src/Console/MailerTestCommand.php b/src/Console/MailerTestCommand.php index f7c43949..83a66962 100644 --- a/src/Console/MailerTestCommand.php +++ b/src/Console/MailerTestCommand.php @@ -13,20 +13,15 @@ use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Mime\Address; -class MailerTestCommand extends Command +final class MailerTestCommand extends Command { - protected EmailManager $emailManager; - - /** - * @param EmailManager $emailManager - */ - public function __construct(EmailManager $emailManager) - { - parent::__construct(); - $this->emailManager = $emailManager; + public function __construct( + private readonly EmailManager $emailManager, + ?string $name = null + ) { + parent::__construct($name); } - protected function configure(): void { $this->setName('mailer:send:test') diff --git a/src/Console/NodeApplyUniversalFieldsCommand.php b/src/Console/NodeApplyUniversalFieldsCommand.php index a136ac45..942e2541 100644 --- a/src/Console/NodeApplyUniversalFieldsCommand.php +++ b/src/Console/NodeApplyUniversalFieldsCommand.php @@ -15,20 +15,14 @@ use Symfony\Component\Console\Question\ConfirmationQuestion; use Symfony\Component\Console\Style\SymfonyStyle; -class NodeApplyUniversalFieldsCommand extends Command +final class NodeApplyUniversalFieldsCommand extends Command { - protected ManagerRegistry $managerRegistry; - protected UniversalDataDuplicator $universalDataDuplicator; - - /** - * @param ManagerRegistry $managerRegistry - * @param UniversalDataDuplicator $universalDataDuplicator - */ - public function __construct(ManagerRegistry $managerRegistry, UniversalDataDuplicator $universalDataDuplicator) - { - parent::__construct(); - $this->managerRegistry = $managerRegistry; - $this->universalDataDuplicator = $universalDataDuplicator; + public function __construct( + private readonly ManagerRegistry $managerRegistry, + private readonly UniversalDataDuplicator $universalDataDuplicator, + ?string $name = null + ) { + parent::__construct($name); } protected function configure(): void diff --git a/src/Console/NodeClearTagCommand.php b/src/Console/NodeClearTagCommand.php index 7ba65879..e7e775f7 100644 --- a/src/Console/NodeClearTagCommand.php +++ b/src/Console/NodeClearTagCommand.php @@ -15,18 +15,13 @@ use Symfony\Component\Console\Question\ConfirmationQuestion; use Symfony\Component\Console\Style\SymfonyStyle; -class NodeClearTagCommand extends Command +final class NodeClearTagCommand extends Command { - protected SymfonyStyle $io; - protected ManagerRegistry $managerRegistry; - - /** - * @param ManagerRegistry $managerRegistry - */ - public function __construct(ManagerRegistry $managerRegistry) - { - parent::__construct(); - $this->managerRegistry = $managerRegistry; + public function __construct( + private readonly ManagerRegistry $managerRegistry, + ?string $name = null + ) { + parent::__construct($name); } protected function configure(): void @@ -48,7 +43,7 @@ protected function getNodeQueryBuilder(Tag $tag): QueryBuilder protected function execute(InputInterface $input, OutputInterface $output): int { $em = $this->managerRegistry->getManagerForClass(Node::class); - $this->io = new SymfonyStyle($input, $output); + $io = new SymfonyStyle($input, $output); $tagId = (int) $input->getArgument('tagId'); if ($tagId <= 0) { @@ -69,12 +64,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int ->getSingleScalarResult(); if ($count <= 0) { - $this->io->warning('No nodes were found linked with this tag.'); + $io->warning('No nodes were found linked with this tag.'); return 0; } if ( - $this->io->askQuestion(new ConfirmationQuestion( + $io->askQuestion(new ConfirmationQuestion( sprintf('Are you sure to delete permanently %d nodes?', $count), false )) @@ -84,7 +79,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int ->getQuery() ->getResult(); - $this->io->progressStart($count); + $io->progressStart($count); /** @var Node $node */ foreach ($results as $node) { $em->remove($node); @@ -92,10 +87,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int $em->flush(); // Executes all updates. } ++$i; - $this->io->progressAdvance(); + $io->progressAdvance(); } $em->flush(); - $this->io->progressFinish(); + $io->progressFinish(); } return 0; diff --git a/src/Console/NodeTypesCommand.php b/src/Console/NodeTypesCommand.php index 23107664..262bcaa6 100644 --- a/src/Console/NodeTypesCommand.php +++ b/src/Console/NodeTypesCommand.php @@ -13,20 +13,13 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; -/** - * Command line utils for managing node-types from terminal. - */ -class NodeTypesCommand extends Command +final class NodeTypesCommand extends Command { - protected ManagerRegistry $managerRegistry; - - /** - * @param ManagerRegistry $managerRegistry - */ - public function __construct(ManagerRegistry $managerRegistry) - { - parent::__construct(); - $this->managerRegistry = $managerRegistry; + public function __construct( + private readonly ManagerRegistry $managerRegistry, + ?string $name = null + ) { + parent::__construct($name); } protected function configure(): void diff --git a/src/Console/NodeTypesCreationCommand.php b/src/Console/NodeTypesCreationCommand.php index 81c11ba2..25788fec 100644 --- a/src/Console/NodeTypesCreationCommand.php +++ b/src/Console/NodeTypesCreationCommand.php @@ -23,21 +23,13 @@ */ class NodeTypesCreationCommand extends Command { - protected ManagerRegistry $managerRegistry; - protected HandlerFactory $handlerFactory; - protected SchemaUpdater $schemaUpdater; - - /** - * @param ManagerRegistry $managerRegistry - * @param HandlerFactory $handlerFactory - * @param SchemaUpdater $schemaUpdater - */ - public function __construct(ManagerRegistry $managerRegistry, HandlerFactory $handlerFactory, SchemaUpdater $schemaUpdater) - { - parent::__construct(); - $this->managerRegistry = $managerRegistry; - $this->handlerFactory = $handlerFactory; - $this->schemaUpdater = $schemaUpdater; + public function __construct( + protected readonly ManagerRegistry $managerRegistry, + protected readonly HandlerFactory $handlerFactory, + protected readonly SchemaUpdater $schemaUpdater, + ?string $name = null + ) { + parent::__construct($name); } protected function configure(): void @@ -108,7 +100,8 @@ private function executeCreation(InputInterface $input, OutputInterface $output) protected function addNodeTypeField(NodeType $nodeType, int|float|string $position, SymfonyStyle $io): void { $field = new NodeTypeField(); - $field->setPosition((float) $position); + $position = floatval($position); + $field->setPosition($position); $questionfName = new Question('[Field ' . $position . '] Enter field name', 'content'); $fName = $io->askQuestion($questionfName); diff --git a/src/Console/NodeTypesDefaultValuesCommand.php b/src/Console/NodeTypesDefaultValuesCommand.php index bf59585e..2c4c3ec7 100644 --- a/src/Console/NodeTypesDefaultValuesCommand.php +++ b/src/Console/NodeTypesDefaultValuesCommand.php @@ -5,7 +5,6 @@ namespace RZ\Roadiz\CoreBundle\Console; use Doctrine\Persistence\ManagerRegistry; -use RZ\Roadiz\Core\AbstractEntities\AbstractEntity; use RZ\Roadiz\Core\AbstractEntities\AbstractField; use RZ\Roadiz\CoreBundle\Entity\NodeTypeField; use RZ\Roadiz\EntityGenerator\Field\DefaultValuesResolverInterface; @@ -15,16 +14,14 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; -class NodeTypesDefaultValuesCommand extends Command +final class NodeTypesDefaultValuesCommand extends Command { - private DefaultValuesResolverInterface $defaultValuesResolver; - private ManagerRegistry $managerRegistry; - - public function __construct(DefaultValuesResolverInterface $defaultValuesResolver, ManagerRegistry $managerRegistry, string $name = null) - { + public function __construct( + private readonly DefaultValuesResolverInterface $defaultValuesResolver, + private readonly ManagerRegistry $managerRegistry, + string $name = null + ) { parent::__construct($name); - $this->defaultValuesResolver = $defaultValuesResolver; - $this->managerRegistry = $managerRegistry; } protected function configure(): void diff --git a/src/Console/NodeTypesDeleteCommand.php b/src/Console/NodeTypesDeleteCommand.php index e4bfb545..6e9d47c0 100644 --- a/src/Console/NodeTypesDeleteCommand.php +++ b/src/Console/NodeTypesDeleteCommand.php @@ -19,23 +19,15 @@ /** * Command line utils for managing node-types from terminal. */ -class NodeTypesDeleteCommand extends Command +final class NodeTypesDeleteCommand extends Command { - protected ManagerRegistry $managerRegistry; - protected HandlerFactory $handlerFactory; - protected SchemaUpdater $schemaUpdater; - - /** - * @param ManagerRegistry $managerRegistry - * @param HandlerFactory $handlerFactory - * @param SchemaUpdater $schemaUpdater - */ - public function __construct(ManagerRegistry $managerRegistry, HandlerFactory $handlerFactory, SchemaUpdater $schemaUpdater) - { - parent::__construct(); - $this->managerRegistry = $managerRegistry; - $this->handlerFactory = $handlerFactory; - $this->schemaUpdater = $schemaUpdater; + public function __construct( + private readonly ManagerRegistry $managerRegistry, + private readonly HandlerFactory $handlerFactory, + private readonly SchemaUpdater $schemaUpdater, + ?string $name = null + ) { + parent::__construct($name); } protected function configure(): void diff --git a/src/Console/NodesCleanNamesCommand.php b/src/Console/NodesCleanNamesCommand.php index bee548c7..394a55de 100644 --- a/src/Console/NodesCleanNamesCommand.php +++ b/src/Console/NodesCleanNamesCommand.php @@ -15,21 +15,14 @@ use Symfony\Component\Console\Question\ConfirmationQuestion; use Symfony\Component\Console\Style\SymfonyStyle; -/** - * @package RZ\Roadiz\CoreBundle\Console - */ final class NodesCleanNamesCommand extends Command { - protected NodeNamePolicyInterface $nodeNamePolicy; - protected ManagerRegistry $managerRegistry; - - /** - * @param ManagerRegistry $managerRegistry - */ - public function __construct(ManagerRegistry $managerRegistry) - { - parent::__construct(); - $this->managerRegistry = $managerRegistry; + public function __construct( + private readonly ManagerRegistry $managerRegistry, + private readonly NodeNamePolicyInterface $nodeNamePolicy, + ?string $name = null + ) { + parent::__construct($name); } protected function configure(): void diff --git a/src/Console/NodesCommand.php b/src/Console/NodesCommand.php index 3442ea0c..cd5e56a0 100644 --- a/src/Console/NodesCommand.php +++ b/src/Console/NodesCommand.php @@ -13,20 +13,13 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; -/** - * Command line utils for managing nodes from terminal. - */ -class NodesCommand extends Command +final class NodesCommand extends Command { - protected ManagerRegistry $managerRegistry; - - /** - * @param ManagerRegistry $managerRegistry - */ - public function __construct(ManagerRegistry $managerRegistry) - { - parent::__construct(); - $this->managerRegistry = $managerRegistry; + public function __construct( + private readonly ManagerRegistry $managerRegistry, + ?string $name = null + ) { + parent::__construct($name); } protected function configure(): void diff --git a/src/Console/NodesCreationCommand.php b/src/Console/NodesCreationCommand.php deleted file mode 100644 index 98e3ce6b..00000000 --- a/src/Console/NodesCreationCommand.php +++ /dev/null @@ -1,133 +0,0 @@ -managerRegistry = $managerRegistry; - $this->nodeFactory = $nodeFactory; - } - - protected function configure(): void - { - $this->setName('nodes:create') - ->setDescription('Create a new node') - ->addArgument( - 'node-name', - InputArgument::REQUIRED, - 'Node name' - ) - ->addArgument( - 'node-type', - InputArgument::REQUIRED, - 'Node-type name' - ) - ->addArgument( - 'locale', - InputArgument::OPTIONAL, - 'Translation locale' - ); - } - - protected function execute(InputInterface $input, OutputInterface $output): int - { - $nodeName = $input->getArgument('node-name'); - $typeName = $input->getArgument('node-type'); - $locale = $input->getArgument('locale'); - $this->io = new SymfonyStyle($input, $output); - - $existingNode = $this->managerRegistry - ->getRepository(Node::class) - ->setDisplayingNotPublishedNodes(true) - ->findOneByNodeName($nodeName); - - if (null === $existingNode) { - $type = $this->managerRegistry - ->getRepository(NodeType::class) - ->findOneByName($typeName); - - if (null !== $type) { - $translation = null; - - if ($locale) { - $translation = $this->managerRegistry - ->getRepository(TranslationInterface::class) - ->findOneBy(['locale' => $locale]); - } - - if ($translation === null) { - $translation = $this->managerRegistry - ->getRepository(TranslationInterface::class) - ->findDefault(); - } - - $this->executeNodeCreation($input->getArgument('node-name'), $type, $translation); - } else { - $this->io->error('"' . $typeName . '" node type does not exist.'); - return 1; - } - return 0; - } else { - $this->io->error($existingNode->getNodeName() . ' node already exists.'); - return 1; - } - } - - /** - * @param string $nodeName - * @param NodeType $type - * @param TranslationInterface $translation - */ - private function executeNodeCreation( - string $nodeName, - NodeType $type, - TranslationInterface $translation - ): void { - $node = $this->nodeFactory->create($nodeName, $type, $translation); - $source = $node->getNodeSources()->first() ?: null; - if (null === $source) { - throw new \InvalidArgumentException('Node source is null'); - } - $fields = $type->getFields(); - - foreach ($fields as $field) { - if (!$field->isVirtual()) { - $question = new Question('[Field ' . $field->getLabel() . '] : ', null); - $fValue = $this->io->askQuestion($question); - $setterName = $field->getSetterName(); - $source->$setterName($fValue); - } - } - - $this->managerRegistry->getManagerForClass(Node::class)->flush(); - $this->io->success('Node “' . $nodeName . '” created at root level.'); - } -} diff --git a/src/Console/NodesDetailsCommand.php b/src/Console/NodesDetailsCommand.php index d5095807..53515526 100644 --- a/src/Console/NodesDetailsCommand.php +++ b/src/Console/NodesDetailsCommand.php @@ -14,17 +14,13 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; -class NodesDetailsCommand extends Command +final class NodesDetailsCommand extends Command { - protected ManagerRegistry $managerRegistry; - - /** - * @param ManagerRegistry $managerRegistry - */ - public function __construct(ManagerRegistry $managerRegistry) - { - parent::__construct(); - $this->managerRegistry = $managerRegistry; + public function __construct( + private readonly ManagerRegistry $managerRegistry, + ?string $name = null + ) { + parent::__construct($name); } protected function configure(): void @@ -67,7 +63,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int if (is_array($data)) { $data = implode(', ', $data); } - if ($data instanceof \DateTime) { + if ($data instanceof \DateTimeInterface) { $data = $data->format('c'); } if ($data instanceof \stdClass) { diff --git a/src/Console/NodesEmptyTrashCommand.php b/src/Console/NodesEmptyTrashCommand.php index d9078595..8e21028b 100644 --- a/src/Console/NodesEmptyTrashCommand.php +++ b/src/Console/NodesEmptyTrashCommand.php @@ -15,20 +15,14 @@ use Symfony\Component\Console\Question\ConfirmationQuestion; use Symfony\Component\Console\Style\SymfonyStyle; -class NodesEmptyTrashCommand extends Command +final class NodesEmptyTrashCommand extends Command { - protected ManagerRegistry $managerRegistry; - protected HandlerFactory $handlerFactory; - - /** - * @param ManagerRegistry $managerRegistry - * @param HandlerFactory $handlerFactory - */ - public function __construct(ManagerRegistry $managerRegistry, HandlerFactory $handlerFactory) - { - parent::__construct(); - $this->managerRegistry = $managerRegistry; - $this->handlerFactory = $handlerFactory; + public function __construct( + private readonly ManagerRegistry $managerRegistry, + private readonly HandlerFactory $handlerFactory, + ?string $name = null + ) { + parent::__construct($name); } protected function configure(): void diff --git a/src/Console/NodesOrphansCommand.php b/src/Console/NodesOrphansCommand.php index 60fcf003..e3a68598 100644 --- a/src/Console/NodesOrphansCommand.php +++ b/src/Console/NodesOrphansCommand.php @@ -13,20 +13,13 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; -/** - * @package RZ\Roadiz\CoreBundle\Console - */ -class NodesOrphansCommand extends Command +final class NodesOrphansCommand extends Command { - protected ManagerRegistry $managerRegistry; - - /** - * @param ManagerRegistry $managerRegistry - */ - public function __construct(ManagerRegistry $managerRegistry) - { - parent::__construct(); - $this->managerRegistry = $managerRegistry; + public function __construct( + private readonly ManagerRegistry $managerRegistry, + ?string $name = null + ) { + parent::__construct($name); } protected function configure(): void diff --git a/src/Console/PrivateKeyCommand.php b/src/Console/PrivateKeyCommand.php deleted file mode 100644 index 9e368498..00000000 --- a/src/Console/PrivateKeyCommand.php +++ /dev/null @@ -1,57 +0,0 @@ -keyChain = $keyChain; - } - - protected function configure(): void - { - $this->setName('crypto:private-key:info') - ->addArgument('key-name', InputArgument::REQUIRED) - ->setDescription('Get a private or public key information') - ; - } - - protected function execute(InputInterface $input, OutputInterface $output): int - { - $io = new SymfonyStyle($input, $output); - - $keyName = $input->getArgument('key-name'); - $key = $this->keyChain->get($keyName); - - $io->table([ - 'name', - 'type', - 'derivation', - 'usage', - 'base64', - ], [[ - $keyName, - $key->isAsymmetricKey() ? 'asymmetric' : 'symmetric', - $key->isPublicKey() ? 'public' : 'private', - $key->isSigningKey() ? 'signing' : 'encryption', - base64_encode($key->getRawKeyMaterial()) - ]]); - return 0; - } -} diff --git a/src/Console/PurgeLoginAttemptCommand.php b/src/Console/PurgeLoginAttemptCommand.php deleted file mode 100644 index 95985673..00000000 --- a/src/Console/PurgeLoginAttemptCommand.php +++ /dev/null @@ -1,54 +0,0 @@ -managerRegistry = $managerRegistry; - } - - protected function configure(): void - { - $this->setName('login-attempts:purge') - ->setDescription('Purge all login attempts for one IP address') - ->addArgument( - 'ip-address', - InputArgument::REQUIRED - ); - } - - protected function execute(InputInterface $input, OutputInterface $output): int - { - $io = new SymfonyStyle($input, $output); - $this->managerRegistry - ->getRepository(LoginAttempt::class) - ->purgeLoginAttempts($input->getArgument('ip-address')); - - $io->success('All login attempts were deleted for ' . $input->getArgument('ip-address')); - - return 0; - } -} diff --git a/src/Console/RegisterCronLastExecDateCommand.php b/src/Console/RegisterCronLastExecDateCommand.php new file mode 100644 index 00000000..20bed441 --- /dev/null +++ b/src/Console/RegisterCronLastExecDateCommand.php @@ -0,0 +1,46 @@ +managerRegistry->getManager(); + $parameter = $this->settingRepository->findOneByName('cron_last_exec_date'); + if (null === $parameter) { + $parameter = new Setting(); + $parameter->setName('cron_last_exec_date'); + $manager->persist($parameter); + } + + $parameter->setValue(new \DateTimeImmutable()); + $manager->flush(); + $io->success('Last execution date of cron job has been persisted.'); + + return Command::SUCCESS; + } +} diff --git a/src/Console/SolrCommand.php b/src/Console/SolrCommand.php index 6e930a20..32396f2b 100644 --- a/src/Console/SolrCommand.php +++ b/src/Console/SolrCommand.php @@ -10,21 +10,15 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; -/** - * Command line utils for managing nodes from terminal. - */ class SolrCommand extends Command { protected ?SymfonyStyle $io = null; - protected ClientRegistry $clientRegistry; - /** - * @param ClientRegistry $clientRegistry - */ - public function __construct(ClientRegistry $clientRegistry) - { - parent::__construct(); - $this->clientRegistry = $clientRegistry; + public function __construct( + protected readonly ClientRegistry $clientRegistry, + ?string $name = null + ) { + parent::__construct($name); } protected function configure(): void @@ -57,7 +51,7 @@ protected function displayBasicConfig(): void if (null !== $this->io) { $this->io->error('No Solr search engine server has been configured…'); $this->io->note(<<indexerFactory = $indexerFactory; + public function __construct( + protected readonly IndexerFactoryInterface $indexerFactory, + ClientRegistry $clientRegistry, + ?string $name = null + ) { + parent::__construct($clientRegistry, $name); } protected function configure(): void diff --git a/src/Console/SolrReindexCommand.php b/src/Console/SolrReindexCommand.php index eb582902..041a22fb 100644 --- a/src/Console/SolrReindexCommand.php +++ b/src/Console/SolrReindexCommand.php @@ -12,31 +12,20 @@ use RZ\Roadiz\CoreBundle\SearchEngine\Indexer\IndexerFactoryInterface; use RZ\Roadiz\CoreBundle\SearchEngine\SolariumDocumentTranslation; use RZ\Roadiz\CoreBundle\SearchEngine\SolariumNodeSource; -use Symfony\Component\Console\Helper\QuestionHelper; 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\Stopwatch\Stopwatch; -/** - * Command line utils for managing nodes from terminal. - */ class SolrReindexCommand extends SolrCommand implements ThemeAwareCommandInterface { - protected ?QuestionHelper $questionHelper = null; - protected IndexerFactoryInterface $indexerFactory; - - /** - * @param ClientRegistry $clientRegistry - * @param IndexerFactoryInterface $indexerFactory - */ public function __construct( + protected readonly IndexerFactoryInterface $indexerFactory, ClientRegistry $clientRegistry, - IndexerFactoryInterface $indexerFactory + ?string $name = null ) { - parent::__construct($clientRegistry); - $this->indexerFactory = $indexerFactory; + parent::__construct($clientRegistry, $name); } protected function configure(): void diff --git a/src/Console/SolrResetCommand.php b/src/Console/SolrResetCommand.php index 6cd34a0f..ac48d1f3 100644 --- a/src/Console/SolrResetCommand.php +++ b/src/Console/SolrResetCommand.php @@ -13,21 +13,14 @@ use Symfony\Component\Console\Question\ConfirmationQuestion; use Symfony\Component\Console\Style\SymfonyStyle; -/** - * Command line utils for managing nodes from terminal. - */ -class SolrResetCommand extends SolrCommand +final class SolrResetCommand extends SolrCommand { - protected IndexerFactoryInterface $indexerFactory; - - /** - * @param ClientRegistry $clientRegistry - * @param IndexerFactoryInterface $indexerFactory - */ - public function __construct(ClientRegistry $clientRegistry, IndexerFactoryInterface $indexerFactory) - { - parent::__construct($clientRegistry); - $this->indexerFactory = $indexerFactory; + public function __construct( + private readonly IndexerFactoryInterface $indexerFactory, + ClientRegistry $clientRegistry, + ?string $name = null + ) { + parent::__construct($clientRegistry, $name); } protected function configure(): void diff --git a/src/Console/TranslationsCommand.php b/src/Console/TranslationsCommand.php index 58217357..e3781525 100644 --- a/src/Console/TranslationsCommand.php +++ b/src/Console/TranslationsCommand.php @@ -11,20 +11,13 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; -/** - * Command line utils for managing translations from terminal. - */ class TranslationsCommand extends Command { - protected ManagerRegistry $managerRegistry; - - /** - * @param ManagerRegistry $managerRegistry - */ - public function __construct(ManagerRegistry $managerRegistry) - { - parent::__construct(); - $this->managerRegistry = $managerRegistry; + public function __construct( + protected readonly ManagerRegistry $managerRegistry, + ?string $name = null + ) { + parent::__construct($name); } protected function configure(): void diff --git a/src/Console/TranslationsCreationCommand.php b/src/Console/TranslationsCreationCommand.php index a4ac9e5d..9d0f8b90 100644 --- a/src/Console/TranslationsCreationCommand.php +++ b/src/Console/TranslationsCreationCommand.php @@ -4,9 +4,7 @@ namespace RZ\Roadiz\CoreBundle\Console; -use Doctrine\Persistence\ManagerRegistry; use RZ\Roadiz\CoreBundle\Entity\Translation; -use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -16,19 +14,8 @@ /** * Command line utils for managing translations */ -class TranslationsCreationCommand extends Command +final class TranslationsCreationCommand extends TranslationsCommand { - protected ManagerRegistry $managerRegistry; - - /** - * @param ManagerRegistry $managerRegistry - */ - public function __construct(ManagerRegistry $managerRegistry) - { - parent::__construct(); - $this->managerRegistry = $managerRegistry; - } - protected function configure(): void { $this->setName('translations:create') diff --git a/src/Console/TranslationsDeleteCommand.php b/src/Console/TranslationsDeleteCommand.php index eb3e5a9d..60f54562 100644 --- a/src/Console/TranslationsDeleteCommand.php +++ b/src/Console/TranslationsDeleteCommand.php @@ -4,9 +4,7 @@ namespace RZ\Roadiz\CoreBundle\Console; -use Doctrine\Persistence\ManagerRegistry; use RZ\Roadiz\CoreBundle\Entity\Translation; -use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -16,19 +14,8 @@ /** * Command line utils for managing translations. */ -class TranslationsDeleteCommand extends Command +final class TranslationsDeleteCommand extends TranslationsCommand { - protected ManagerRegistry $managerRegistry; - - /** - * @param ManagerRegistry $managerRegistry - */ - public function __construct(ManagerRegistry $managerRegistry) - { - parent::__construct(); - $this->managerRegistry = $managerRegistry; - } - protected function configure(): void { $this->setName('translations:delete') diff --git a/src/Console/TranslationsDisableCommand.php b/src/Console/TranslationsDisableCommand.php index abf6317b..164b7422 100644 --- a/src/Console/TranslationsDisableCommand.php +++ b/src/Console/TranslationsDisableCommand.php @@ -4,9 +4,7 @@ namespace RZ\Roadiz\CoreBundle\Console; -use Doctrine\Persistence\ManagerRegistry; use RZ\Roadiz\CoreBundle\Entity\Translation; -use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -16,19 +14,8 @@ /** * Command line utils for managing translations. */ -class TranslationsDisableCommand extends Command +final class TranslationsDisableCommand extends TranslationsCommand { - protected ManagerRegistry $managerRegistry; - - /** - * @param ManagerRegistry $managerRegistry - */ - public function __construct(ManagerRegistry $managerRegistry) - { - parent::__construct(); - $this->managerRegistry = $managerRegistry; - } - protected function configure(): void { $this->setName('translations:disable') diff --git a/src/Console/TranslationsEnableCommand.php b/src/Console/TranslationsEnableCommand.php index f7fe0530..f6656694 100644 --- a/src/Console/TranslationsEnableCommand.php +++ b/src/Console/TranslationsEnableCommand.php @@ -4,9 +4,7 @@ namespace RZ\Roadiz\CoreBundle\Console; -use Doctrine\Persistence\ManagerRegistry; use RZ\Roadiz\CoreBundle\Entity\Translation; -use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -16,19 +14,8 @@ /** * Command line utils for managing translations. */ -class TranslationsEnableCommand extends Command +final class TranslationsEnableCommand extends TranslationsCommand { - protected ManagerRegistry $managerRegistry; - - /** - * @param ManagerRegistry $managerRegistry - */ - public function __construct(ManagerRegistry $managerRegistry) - { - parent::__construct(); - $this->managerRegistry = $managerRegistry; - } - protected function configure(): void { $this->setName('translations:enable') diff --git a/src/Console/UsersCommand.php b/src/Console/UsersCommand.php index f86c7100..2309ff8d 100644 --- a/src/Console/UsersCommand.php +++ b/src/Console/UsersCommand.php @@ -14,20 +14,13 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; -/** - * Command line utils for managing users from terminal. - */ class UsersCommand extends Command { - protected ManagerRegistry $managerRegistry; - - /** - * @param ManagerRegistry $managerRegistry - */ - public function __construct(ManagerRegistry $managerRegistry) - { - parent::__construct(); - $this->managerRegistry = $managerRegistry; + public function __construct( + protected readonly ManagerRegistry $managerRegistry, + string $name = null + ) { + parent::__construct($name); } protected function configure(): void @@ -68,7 +61,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($user === null) { $io->error('User “' . $name . '” does not exist… use users:create to add a new user.'); } else { - $tableContent = [$this->getUserTableRow($user)]; + $tableContent = [ + $this->getUserTableRow($user), + ]; $io->table( array_keys($tableContent[0]), $tableContent @@ -120,10 +115,9 @@ protected function getUserForInput(InputInterface $input): User * Get role by name, and create it if it does not exist. * * @param string $roleName - * * @return Role */ - public function getRole(string $roleName = Role::ROLE_SUPERADMIN) + public function getRole(string $roleName = Role::ROLE_SUPERADMIN): Role { $role = $this->managerRegistry ->getRepository(Role::class) diff --git a/src/Console/UsersCreationCommand.php b/src/Console/UsersCreationCommand.php index cd97c713..2373093e 100644 --- a/src/Console/UsersCreationCommand.php +++ b/src/Console/UsersCreationCommand.php @@ -15,9 +15,6 @@ use Symfony\Component\Console\Question\Question; use Symfony\Component\Console\Style\SymfonyStyle; -/** - * Command line utils for managing users from terminal. - */ final class UsersCreationCommand extends UsersCommand { protected function configure(): void @@ -38,29 +35,32 @@ protected function configure(): void protected function execute(InputInterface $input, OutputInterface $output): int { + $io = new SymfonyStyle($input, $output); $name = $input->getArgument('username'); - if ($name) { - /** @var User|null $user */ - $user = $this->managerRegistry - ->getRepository(User::class) - ->findOneBy(['username' => $name]); - - if (null === $user) { - $user = $this->executeUserCreation($name, $input, $output); - - // Change password right away - $command = $this->getApplication()->find('users:password'); - $arguments = [ - 'username' => $user->getUsername(), - ]; - $passwordInput = new ArrayInput($arguments); - return $command->run($passwordInput, $output); - } else { - throw new \InvalidArgumentException('User “' . $name . '” already exists.'); - } + if (!\is_string($name) || empty($name)) { + throw new \InvalidArgumentException('Username argument is required.'); } - return 0; + + /** @var User|null $user */ + $user = $this->managerRegistry + ->getRepository(User::class) + ->findOneBy(['username' => $name]); + + if ($user instanceof User) { + $io->warning('User “' . $name . '” already exists.'); + return 1; + } + + $user = $this->executeUserCreation($name, $input, $output); + + // Change password right away + $command = $this->getApplication()->find('users:password'); + $arguments = [ + 'username' => $user->getUsername(), + ]; + $passwordInput = new ArrayInput($arguments); + return $command->run($passwordInput, $output); } /** diff --git a/src/Console/UsersDeleteCommand.php b/src/Console/UsersDeleteCommand.php index 6fb754da..2e8c3062 100644 --- a/src/Console/UsersDeleteCommand.php +++ b/src/Console/UsersDeleteCommand.php @@ -11,9 +11,6 @@ use Symfony\Component\Console\Question\ConfirmationQuestion; use Symfony\Component\Console\Style\SymfonyStyle; -/** - * Command line utils for managing users from terminal. - */ final class UsersDeleteCommand extends UsersCommand { protected function configure(): void @@ -31,33 +28,24 @@ protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); $name = $input->getArgument('username'); + $user = $this->getUserForInput($input); - if ($name) { - /** @var User|null $user */ - $user = $this->managerRegistry - ->getRepository(User::class) - ->findOneBy(['username' => $name]); - - if (null !== $user) { - $confirmation = new ConfirmationQuestion( - 'Do you really want to delete user “' . $user->getUsername() . '”?', - false - ); - if ( - !$input->isInteractive() || $io->askQuestion( - $confirmation - ) - ) { - $this->managerRegistry->getManagerForClass(User::class)->remove($user); - $this->managerRegistry->getManagerForClass(User::class)->flush(); - $io->success('User “' . $name . '” deleted.'); - } else { - $io->warning('User “' . $name . '” was not deleted.'); - } - } else { - throw new \InvalidArgumentException('User “' . $name . '” does not exist.'); - } + $confirmation = new ConfirmationQuestion( + 'Do you really want to delete user “' . $user->getUsername() . '”?', + false + ); + if ( + !$input->isInteractive() || $io->askQuestion( + $confirmation + ) + ) { + $this->managerRegistry->getManagerForClass(User::class)->remove($user); + $this->managerRegistry->getManagerForClass(User::class)->flush(); + $io->success('User “' . $name . '” deleted.'); + return 0; + } else { + $io->warning('User “' . $name . '” was not deleted.'); + return 1; } - return 0; } } diff --git a/src/Console/UsersDisableCommand.php b/src/Console/UsersDisableCommand.php index a5d3c5f7..97b0062a 100644 --- a/src/Console/UsersDisableCommand.php +++ b/src/Console/UsersDisableCommand.php @@ -11,9 +11,6 @@ use Symfony\Component\Console\Question\ConfirmationQuestion; use Symfony\Component\Console\Style\SymfonyStyle; -/** - * Command line utils for managing users from terminal. - */ final class UsersDisableCommand extends UsersCommand { protected function configure(): void @@ -31,33 +28,24 @@ protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); $name = $input->getArgument('username'); + $user = $this->getUserForInput($input); - if ($name) { - /** @var User|null $user */ - $user = $this->managerRegistry - ->getRepository(User::class) - ->findOneBy(['username' => $name]); - - if (null !== $user) { - $confirmation = new ConfirmationQuestion( - 'Do you really want to disable user “' . $user->getUsername() . '”?', - false - ); - if ( - !$input->isInteractive() || $io->askQuestion( - $confirmation - ) - ) { - $user->setEnabled(false); - $this->managerRegistry->getManagerForClass(User::class)->flush(); - $io->success('User “' . $name . '” disabled.'); - } else { - $io->warning('User “' . $name . '” was not disabled.'); - } - } else { - throw new \InvalidArgumentException('User “' . $name . '” does not exist.'); - } + $confirmation = new ConfirmationQuestion( + 'Do you really want to disable user “' . $user->getUsername() . '”?', + false + ); + if ( + !$input->isInteractive() || $io->askQuestion( + $confirmation + ) + ) { + $user->setEnabled(false); + $this->managerRegistry->getManagerForClass(User::class)->flush(); + $io->success('User “' . $name . '” disabled.'); + return 0; + } else { + $io->warning('User “' . $name . '” was not disabled.'); + return 1; } - return 0; } } diff --git a/src/Console/UsersEnableCommand.php b/src/Console/UsersEnableCommand.php index f166e74b..b9092656 100644 --- a/src/Console/UsersEnableCommand.php +++ b/src/Console/UsersEnableCommand.php @@ -11,9 +11,6 @@ use Symfony\Component\Console\Question\ConfirmationQuestion; use Symfony\Component\Console\Style\SymfonyStyle; -/** - * Command line utils for managing users from terminal. - */ final class UsersEnableCommand extends UsersCommand { protected function configure(): void @@ -31,32 +28,24 @@ protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); $name = $input->getArgument('username'); + $user = $this->getUserForInput($input); - if ($name) { - $user = $this->managerRegistry - ->getRepository(User::class) - ->findOneBy(['username' => $name]); - - if (null !== $user) { - $confirmation = new ConfirmationQuestion( - 'Do you really want to enable user “' . $user->getUsername() . '”?', - false - ); - if ( - !$input->isInteractive() || $io->askQuestion( - $confirmation - ) - ) { - $user->setEnabled(true); - $this->managerRegistry->getManagerForClass(User::class)->flush(); - $io->success('User “' . $name . '” was enabled.'); - } else { - $io->warning('User “' . $name . '” was not enabled'); - } - } else { - throw new \InvalidArgumentException('User “' . $name . '” does not exist.'); - } + $confirmation = new ConfirmationQuestion( + 'Do you really want to enable user “' . $user->getUsername() . '”?', + false + ); + if ( + !$input->isInteractive() || $io->askQuestion( + $confirmation + ) + ) { + $user->setEnabled(true); + $this->managerRegistry->getManagerForClass(User::class)->flush(); + $io->success('User “' . $name . '” was enabled.'); + return 0; + } else { + $io->warning('User “' . $name . '” was not enabled'); + return 1; } - return 0; } } diff --git a/src/Console/UsersPasswordCommand.php b/src/Console/UsersPasswordCommand.php index 6472dfd7..9d265e1f 100644 --- a/src/Console/UsersPasswordCommand.php +++ b/src/Console/UsersPasswordCommand.php @@ -13,21 +13,14 @@ use Symfony\Component\Console\Question\ConfirmationQuestion; use Symfony\Component\Console\Style\SymfonyStyle; -/** - * Command line utils for managing users from terminal. - */ final class UsersPasswordCommand extends UsersCommand { - private PasswordGenerator $passwordGenerator; - - /** - * @param ManagerRegistry $managerRegistry - * @param PasswordGenerator $passwordGenerator - */ - public function __construct(ManagerRegistry $managerRegistry, PasswordGenerator $passwordGenerator) - { - parent::__construct($managerRegistry); - $this->passwordGenerator = $passwordGenerator; + public function __construct( + private readonly PasswordGenerator $passwordGenerator, + ManagerRegistry $managerRegistry, + ?string $name = null + ) { + parent::__construct($managerRegistry, $name); } protected function configure(): void @@ -45,33 +38,24 @@ protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); $name = $input->getArgument('username'); - - if ($name) { - /** @var User|null $user */ - $user = $this->managerRegistry - ->getRepository(User::class) - ->findOneBy(['username' => $name]); - - if (null !== $user) { - $confirmation = new ConfirmationQuestion( - 'Do you really want to regenerate user “' . $user->getUsername() . '” password?', - false - ); - if ( - !$input->isInteractive() || $io->askQuestion( - $confirmation - ) - ) { - $user->setPlainPassword($this->passwordGenerator->generatePassword(12)); - $this->managerRegistry->getManagerForClass(User::class)->flush(); - $io->success('A new password was regenerated for ' . $name . ': ' . $user->getPlainPassword()); - } else { - $io->warning('User password was not changed.'); - } - } else { - throw new \InvalidArgumentException('User “' . $name . '” does not exist.'); - } + $user = $this->getUserForInput($input); + + $confirmation = new ConfirmationQuestion( + 'Do you really want to regenerate user “' . $user->getUsername() . '” password?', + false + ); + if ( + !$input->isInteractive() || $io->askQuestion( + $confirmation + ) + ) { + $user->setPlainPassword($this->passwordGenerator->generatePassword(12)); + $this->managerRegistry->getManagerForClass(User::class)->flush(); + $io->success('A new password was regenerated for ' . $name . ': ' . $user->getPlainPassword()); + return 0; + } else { + $io->warning('User password was not changed.'); + return 1; } - return 0; } } diff --git a/src/Console/UsersRolesCommand.php b/src/Console/UsersRolesCommand.php index 40b725b9..047a2d5b 100644 --- a/src/Console/UsersRolesCommand.php +++ b/src/Console/UsersRolesCommand.php @@ -20,16 +20,12 @@ */ final class UsersRolesCommand extends UsersCommand { - private Roles $rolesBag; - - /** - * @param ManagerRegistry $managerRegistry - * @param Roles $rolesBag - */ - public function __construct(ManagerRegistry $managerRegistry, Roles $rolesBag) - { - parent::__construct($managerRegistry); - $this->rolesBag = $rolesBag; + public function __construct( + private readonly Roles $rolesBag, + ManagerRegistry $managerRegistry, + ?string $name = null + ) { + parent::__construct($managerRegistry, $name); } protected function configure(): void @@ -58,53 +54,43 @@ protected function configure(): void protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); - $name = $input->getArgument('username'); - - if ($name) { - /** @var User|null $user */ - $user = $this->managerRegistry - ->getRepository(User::class) - ->findOneBy(['username' => $name]); + $user = $this->getUserForInput($input); - if (null !== $user) { - if ($input->getOption('add')) { - $roles = $this->managerRegistry - ->getRepository(Role::class) - ->getAllRoleName(); + if ($input->getOption('add')) { + $roles = $this->managerRegistry + ->getRepository(Role::class) + ->getAllRoleName(); - $question = new Question( - 'Enter the role name to add' - ); - $question->setAutocompleterValues($roles); + $question = new Question( + 'Enter the role name to add' + ); + $question->setAutocompleterValues($roles); - do { - $role = $io->askQuestion($question); - if ($role != "") { - $user->addRoleEntity($this->rolesBag->get($role)); - $this->managerRegistry->getManagerForClass(User::class)->flush(); - $io->success('Role: ' . $role . ' added.'); - } - } while ($role != ""); - } elseif ($input->getOption('remove')) { - do { - $roles = $user->getRoles(); - $question = new Question( - 'Enter the role name to remove' - ); - $question->setAutocompleterValues($roles); + do { + $role = $io->askQuestion($question); + if ($role != "") { + $user->addRoleEntity($this->rolesBag->get($role)); + $this->managerRegistry->getManagerForClass(User::class)->flush(); + $io->success('Role: ' . $role . ' added.'); + } + } while ($role != ""); + } elseif ($input->getOption('remove')) { + do { + $roles = $user->getRoles(); + $question = new Question( + 'Enter the role name to remove' + ); + $question->setAutocompleterValues($roles); - $role = $io->askQuestion($question); - if (in_array($role, $roles)) { - $user->removeRoleEntity($this->rolesBag->get($role)); - $this->managerRegistry->getManagerForClass(User::class)->flush(); - $io->success('Role: ' . $role . ' removed.'); - } - } while ($role != ""); + $role = $io->askQuestion($question); + if (in_array($role, $roles)) { + $user->removeRoleEntity($this->rolesBag->get($role)); + $this->managerRegistry->getManagerForClass(User::class)->flush(); + $io->success('Role: ' . $role . ' removed.'); } - } else { - throw new \InvalidArgumentException('User “' . $name . '” does not exist.'); - } + } while ($role != ""); } + return 0; } } diff --git a/src/Console/VersionsPurgeCommand.php b/src/Console/VersionsPurgeCommand.php index 0f83df79..8891e472 100644 --- a/src/Console/VersionsPurgeCommand.php +++ b/src/Console/VersionsPurgeCommand.php @@ -18,15 +18,11 @@ final class VersionsPurgeCommand extends Command { - protected ManagerRegistry $managerRegistry; - - /** - * @param ManagerRegistry $managerRegistry - */ - public function __construct(ManagerRegistry $managerRegistry) - { - parent::__construct(); - $this->managerRegistry = $managerRegistry; + public function __construct( + private readonly ManagerRegistry $managerRegistry, + ?string $name = null + ) { + parent::__construct($name); } protected function configure(): void diff --git a/src/Controller/CustomFormController.php b/src/Controller/CustomFormController.php index 78957bce..b044580a 100644 --- a/src/Controller/CustomFormController.php +++ b/src/Controller/CustomFormController.php @@ -7,20 +7,14 @@ use Doctrine\Persistence\ManagerRegistry; use Exception; use League\Flysystem\FilesystemException; -use League\Flysystem\FilesystemOperator; use Limenius\Liform\LiformInterface; use Psr\Log\LoggerInterface; -use RZ\Roadiz\Core\AbstractEntities\TranslationInterface; use RZ\Roadiz\CoreBundle\Bag\Settings; use RZ\Roadiz\CoreBundle\CustomForm\CustomFormHelperFactory; +use RZ\Roadiz\CoreBundle\CustomForm\Message\CustomFormAnswerNotifyMessage; use RZ\Roadiz\CoreBundle\Entity\CustomForm; -use RZ\Roadiz\CoreBundle\Entity\CustomFormAnswer; use RZ\Roadiz\CoreBundle\Exception\EntityAlreadyExistsException; use RZ\Roadiz\CoreBundle\Form\Error\FormErrorSerializerInterface; -use RZ\Roadiz\CoreBundle\Mailer\EmailManager; -use RZ\Roadiz\CoreBundle\Preview\PreviewResolverInterface; -use RZ\Roadiz\CoreBundle\Repository\TranslationRepository; -use RZ\Roadiz\Documents\Models\DocumentInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Form\FormError; use Symfony\Component\Form\FormInterface; @@ -32,57 +26,25 @@ use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException; -use Symfony\Component\Mailer\Exception\TransportExceptionInterface; -use Symfony\Component\Mime\Address; +use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\RateLimiter\RateLimiterFactory; use Symfony\Component\Serializer\SerializerInterface; -use Symfony\Contracts\Translation\LocaleAwareInterface; use Symfony\Contracts\Translation\TranslatorInterface; -use Twig\Error\LoaderError; -use Twig\Error\RuntimeError; -use Twig\Error\SyntaxError; final class CustomFormController extends AbstractController { - private EmailManager $emailManager; - private Settings $settingsBag; - private LoggerInterface $logger; - private TranslatorInterface $translator; - private CustomFormHelperFactory $customFormHelperFactory; - private LiformInterface $liform; - private SerializerInterface $serializer; - private FormErrorSerializerInterface $formErrorSerializer; - private ManagerRegistry $registry; - private RateLimiterFactory $customFormLimiter; - private FilesystemOperator $documentsStorage; - private PreviewResolverInterface $previewResolver; - public function __construct( - EmailManager $emailManager, - Settings $settingsBag, - LoggerInterface $logger, - TranslatorInterface $translator, - CustomFormHelperFactory $customFormHelperFactory, - LiformInterface $liform, - SerializerInterface $serializer, - FormErrorSerializerInterface $formErrorSerializer, - ManagerRegistry $registry, - RateLimiterFactory $customFormLimiter, - FilesystemOperator $documentsStorage, - PreviewResolverInterface $previewResolver + private readonly Settings $settingsBag, + private readonly LoggerInterface $logger, + private readonly TranslatorInterface $translator, + private readonly CustomFormHelperFactory $customFormHelperFactory, + private readonly LiformInterface $liform, + private readonly SerializerInterface $serializer, + private readonly FormErrorSerializerInterface $formErrorSerializer, + private readonly ManagerRegistry $registry, + private readonly RateLimiterFactory $customFormLimiter, + private readonly MessageBusInterface $messageBus, ) { - $this->emailManager = $emailManager; - $this->settingsBag = $settingsBag; - $this->logger = $logger; - $this->translator = $translator; - $this->customFormHelperFactory = $customFormHelperFactory; - $this->liform = $liform; - $this->serializer = $serializer; - $this->formErrorSerializer = $formErrorSerializer; - $this->registry = $registry; - $this->customFormLimiter = $customFormLimiter; - $this->documentsStorage = $documentsStorage; - $this->previewResolver = $previewResolver; } protected function validateCustomForm(?CustomForm $customForm): void @@ -95,38 +57,6 @@ protected function validateCustomForm(?CustomForm $customForm): void } } - protected function getTranslationFromRequest(?Request $request): TranslationInterface - { - $locale = null; - - if (null !== $request) { - $locale = $request->query->get('_locale'); - - /* - * If no _locale query param is defined check Accept-Language header - */ - if (null === $locale) { - $locale = $request->getPreferredLanguage($this->getTranslationRepository()->getAllLocales()); - } - } - /* - * Then fallback to default CMS locale - */ - if (null === $locale) { - $translation = $this->getTranslationRepository()->findDefault(); - } elseif ($this->previewResolver->isPreview()) { - $translation = $this->getTranslationRepository() - ->findOneByLocaleOrOverrideLocale((string) $locale); - } else { - $translation = $this->getTranslationRepository() - ->findOneAvailableByLocaleOrOverrideLocale((string) $locale); - } - if (null === $translation) { - throw new NotFoundHttpException('No translation for locale ' . $locale); - } - return $translation; - } - /** * @param Request $request * @param int $id @@ -139,11 +69,6 @@ public function definitionAction(Request $request, int $id): JsonResponse $this->validateCustomForm($customForm); $helper = $this->customFormHelperFactory->createHelper($customForm); - $translation = $this->getTranslationFromRequest($request); - $request->setLocale($translation->getPreferredLocale()); - if ($this->translator instanceof LocaleAwareInterface) { - $this->translator->setLocale($translation->getPreferredLocale()); - } $schema = json_encode($this->liform->transform($helper->getForm($request, false, false))); return new JsonResponse( @@ -179,12 +104,6 @@ public function postAction(Request $request, int $id): Response $customForm = $this->registry->getRepository(CustomForm::class)->find($id); $this->validateCustomForm($customForm); - $translation = $this->getTranslationFromRequest($request); - $request->setLocale($translation->getPreferredLocale()); - if ($this->translator instanceof LocaleAwareInterface) { - $this->translator->setLocale($translation->getPreferredLocale()); - } - $mixed = $this->prepareAndHandleCustomFormAssignation( $request, $customForm, @@ -222,9 +141,6 @@ public function postAction(Request $request, int $id): Response * @param int $customFormId * @return Response * @throws FilesystemException - * @throws LoaderError - * @throws RuntimeError - * @throws SyntaxError */ public function addAction(Request $request, int $customFormId): Response { @@ -243,7 +159,7 @@ public function addAction(Request $request, int $customFormId): Response ) ); - if ($mixed instanceof RedirectResponse) { + if ($mixed instanceof Response) { $mixed->prepare($request); return $mixed->send(); } else { @@ -268,60 +184,7 @@ public function sentAction(Request $request, int $customFormId): Response } /** - * Send an answer form by Email. - * - * @param CustomFormAnswer $answer - * @param array $assignation - * @param string|array|null $receiver - * @return bool - * @throws TransportExceptionInterface - * @throws LoaderError - * @throws RuntimeError - * @throws SyntaxError - * @deprecated Use async message handler to send email receipt from CustomFormAnswer. - */ - public function sendAnswer( - CustomFormAnswer $answer, - array $assignation, - $receiver - ): bool { - $defaultSender = $this->settingsBag->get('email_sender'); - $defaultSender = !empty($defaultSender) ? $defaultSender : 'sender@roadiz.io'; - $this->emailManager->setAssignation($assignation); - $this->emailManager->setEmailTemplate('@RoadizCore/email/forms/answerForm.html.twig'); - $this->emailManager->setEmailPlainTextTemplate('@RoadizCore/email/forms/answerForm.txt.twig'); - $this->emailManager->setSubject($assignation['title']); - $this->emailManager->setEmailTitle($assignation['title']); - $this->emailManager->setSender($defaultSender); - - try { - foreach ($answer->getAnswerFields() as $customFormAnswerAttr) { - /** @var DocumentInterface $document */ - foreach ($customFormAnswerAttr->getDocuments() as $document) { - $this->emailManager->addResource( - $this->documentsStorage->readStream($document->getMountPath()), - $document->getFilename(), - $this->documentsStorage->mimeType($document->getMountPath()) - ); - } - } - } catch (FilesystemException $exception) { - $this->logger->error($exception->getMessage()); - } - - if (empty($receiver)) { - $this->emailManager->setReceiver($defaultSender); - } else { - $this->emailManager->setReceiver($receiver); - } - - // Send the message - $this->emailManager->send(); - return true; - } - - /** - * Prepare and handle a CustomForm Form then send a confirm email. + * Prepare and handle a CustomForm Form then send a confirmation email. * * * This method will return an assignation **array** if form is not validated. * * customForm @@ -336,7 +199,7 @@ public function sendAnswer( * @param string|null $emailSender * @param bool $prefix * @return array|Response - * @throws SyntaxError|RuntimeError|LoaderError|FilesystemException + * @throws FilesystemException */ public function prepareAndHandleCustomFormAssignation( Request $request, @@ -364,74 +227,39 @@ public function prepareAndHandleCustomFormAssignation( */ $answer = $helper->parseAnswerFormData($form, null, $request->getClientIp()); - /* - * Prepare field assignation for email content. - */ - $assignation["emailFields"] = [ - ["name" => "ip.address", "value" => $answer->getIp()], - ["name" => "submittedAt", "value" => $answer->getSubmittedAt()->format('Y-m-d H:i:s')], - ]; - $assignation["emailFields"] = array_merge( - $assignation["emailFields"], - $answer->toArray(false) - ); - - $assignation['title'] = $this->translator->trans( - 'new.answer.form.%site%', - ['%site%' => $customFormsEntity->getDisplayName()] - ); - - if (null !== $emailSender && false !== filter_var($emailSender, FILTER_VALIDATE_EMAIL)) { - $assignation['mailContact'] = $emailSender; - } else { - $assignation['mailContact'] = $this->settingsBag->get('email_sender'); + $answerId = $answer->getId(); + if (!is_int($answerId)) { + throw new \RuntimeException('Answer ID is null'); } - /* - * Send answer notification - */ - try { - $receiver = array_filter( - array_map('trim', explode(',', $customFormsEntity->getEmail() ?? '')) - ); - $receiver = array_map(function (string $email) { - return new Address($email); - }, $receiver); - $this->sendAnswer( - $answer, - [ - 'mailContact' => $assignation['mailContact'], - 'fields' => $assignation["emailFields"], - 'customForm' => $customFormsEntity, - 'title' => $this->translator->trans( - 'new.answer.form.%site%', - ['%site%' => $customFormsEntity->getDisplayName()] - ), - ], - $receiver - ); + if (null === $emailSender || false === filter_var($emailSender, FILTER_VALIDATE_EMAIL)) { + $emailSender = $this->settingsBag->get('email_sender'); + } - $msg = $this->translator->trans( - 'customForm.%name%.send', - ['%name%' => $customFormsEntity->getDisplayName()] - ); + $this->messageBus->dispatch(new CustomFormAnswerNotifyMessage( + $answerId, + $this->translator->trans( + 'new.answer.form.%site%', + ['%site%' => $customFormsEntity->getDisplayName()] + ), + $emailSender, + $request->getLocale() + )); + + $msg = $this->translator->trans( + 'customForm.%name%.send', + ['%name%' => $customFormsEntity->getDisplayName()] + ); + if (!$request->attributes->getBoolean('_stateless') && $request->hasPreviousSession()) { $session = $request->getSession(); if ($session instanceof Session) { $session->getFlashBag()->add('confirm', $msg); } - $this->logger->info($msg); - - } catch (TransportExceptionInterface $e) { - // Do not fail if answer has been registered but email has not been sent. - $this->logger->warning('Custom form answer has been registered but email could not been sent.', [ - 'exception' => $e, - 'message' => $e->getMessage(), - 'customForm' => $customFormsEntity->getDisplayName(), - 'answerId' => $answer->getId() - ]); } + $this->logger->info($msg); + return $response; } catch (EntityAlreadyExistsException $e) { $form->addError(new FormError($e->getMessage())); @@ -442,16 +270,4 @@ public function prepareAndHandleCustomFormAssignation( $assignation['formObject'] = $form; return $assignation; } - - protected function getTranslationRepository(): TranslationRepository - { - $repository = $this->registry->getRepository(TranslationInterface::class); - if (!$repository instanceof TranslationRepository) { - throw new \RuntimeException( - 'Translation repository must be instance of ' . - TranslationRepository::class - ); - } - return $repository; - } } diff --git a/src/Controller/RedirectionController.php b/src/Controller/RedirectionController.php index bb478d9c..bffc5dc4 100644 --- a/src/Controller/RedirectionController.php +++ b/src/Controller/RedirectionController.php @@ -16,9 +16,6 @@ final class RedirectionController { private UrlGeneratorInterface $urlGenerator; - /** - * @param UrlGeneratorInterface $urlGenerator - */ public function __construct(UrlGeneratorInterface $urlGenerator) { $this->urlGenerator = $urlGenerator; diff --git a/src/Crypto/UniqueKeyEncoderFactory.php b/src/Crypto/UniqueKeyEncoderFactory.php deleted file mode 100644 index 9747ed07..00000000 --- a/src/Crypto/UniqueKeyEncoderFactory.php +++ /dev/null @@ -1,55 +0,0 @@ -keyChain = $keyChain; - $this->defaultKeyName = $defaultKeyName; - } - - public function getEncoder(?string $keyName = null): UniqueKeyEncoderInterface - { - try { - $keyName = $keyName ?? $this->defaultKeyName; - $key = $this->keyChain->get($keyName); - - if ($key instanceof EncryptionSecretKey) { - $publicKey = $key->derivePublicKey(); - return new AsymmetricUniqueKeyEncoder( - $publicKey, - $key - ); - } elseif ($key instanceof EncryptionKey) { - return new SymmetricUniqueKeyEncoder($key); - } - } catch (\Exception $exception) { - throw new InvalidKey( - sprintf('Key %s is not a valid encryption key', $keyName), - 0, - $exception - ); - } - - throw new InvalidKey(sprintf('Key %s is not a valid encryption key', $keyName)); - } -} diff --git a/src/CustomForm/CustomFormHelper.php b/src/CustomForm/CustomFormHelper.php index c333572c..ce8a28de 100644 --- a/src/CustomForm/CustomFormHelper.php +++ b/src/CustomForm/CustomFormHelper.php @@ -213,7 +213,7 @@ protected function getDocumentFolderForCustomForm(): ?Folder */ private function formValueToString($rawValue): string { - if ($rawValue instanceof \DateTime) { + if ($rawValue instanceof \DateTimeInterface) { return $rawValue->format('Y-m-d H:i:s'); } elseif (is_array($rawValue)) { $values = $rawValue; diff --git a/src/CustomForm/Message/CustomFormAnswerNotifyMessage.php b/src/CustomForm/Message/CustomFormAnswerNotifyMessage.php new file mode 100644 index 00000000..8f092728 --- /dev/null +++ b/src/CustomForm/Message/CustomFormAnswerNotifyMessage.php @@ -0,0 +1,49 @@ +customFormAnswerId = $customFormAnswerId; + $this->title = $title; + $this->senderAddress = $senderAddress; + $this->locale = $locale; + } + + public function getCustomFormAnswerId(): int + { + return $this->customFormAnswerId; + } + + public function getTitle(): string + { + return $this->title; + } + + public function getSenderAddress(): string + { + return $this->senderAddress; + } + + public function getLocale(): string + { + return $this->locale; + } +} diff --git a/src/CustomForm/Message/Handler/CustomFormAnswerNotifyMessageHandler.php b/src/CustomForm/Message/Handler/CustomFormAnswerNotifyMessageHandler.php new file mode 100644 index 00000000..60cc59d7 --- /dev/null +++ b/src/CustomForm/Message/Handler/CustomFormAnswerNotifyMessageHandler.php @@ -0,0 +1,124 @@ +managerRegistry + ->getRepository(CustomFormAnswer::class) + ->find($message->getCustomFormAnswerId()); + + if (!($answer instanceof CustomFormAnswer)) { + throw new UnrecoverableMessageHandlingException('CustomFormAnswer not found'); + } + + $emailFields = [ + ["name" => "ip.address", "value" => $answer->getIp()], + ["name" => "submittedAt", "value" => $answer->getSubmittedAt()->format('Y-m-d H:i:s')], + ]; + $emailFields = array_merge( + $emailFields, + $answer->toArray(false) + ); + + $receiver = array_filter( + array_map('trim', explode(',', $answer->getCustomForm()->getEmail() ?? '')) + ); + $receiver = array_map(function (string $email) { + return new Address($email); + }, $receiver); + $this->sendAnswer( + $answer, + [ + 'mailContact' => $message->getSenderAddress(), + 'fields' => $emailFields, + 'customForm' => $answer->getCustomForm(), + 'title' => $message->getTitle(), + 'requestLocale' => $message->getLocale(), + ], + $receiver + ); + } + + /** + * Send an answer form by Email. + * + * @param CustomFormAnswer $answer + * @param array $assignation + * @param string|array|null $receiver + * @throws TransportExceptionInterface + * @throws LoaderError + * @throws RuntimeError + * @throws SyntaxError + */ + private function sendAnswer( + CustomFormAnswer $answer, + array $assignation, + $receiver + ): void { + $defaultSender = $this->settingsBag->get('email_sender'); + $defaultSender = !empty($defaultSender) ? $defaultSender : 'sender@roadiz.io'; + $this->emailManager->setAssignation($assignation); + $this->emailManager->setEmailTemplate('@RoadizCore/email/forms/answerForm.html.twig'); + $this->emailManager->setEmailPlainTextTemplate('@RoadizCore/email/forms/answerForm.txt.twig'); + $this->emailManager->setSubject($assignation['title']); + $this->emailManager->setEmailTitle($assignation['title']); + $this->emailManager->setSender($defaultSender); + + try { + foreach ($answer->getAnswerFields() as $customFormAnswerAttr) { + /** @var DocumentInterface $document */ + foreach ($customFormAnswerAttr->getDocuments() as $document) { + $this->emailManager->addResource( + $this->documentsStorage->readStream($document->getMountPath()), + $document->getFilename(), + $this->documentsStorage->mimeType($document->getMountPath()) + ); + } + } + } catch (FilesystemException $exception) { + $this->logger->error($exception->getMessage(), [ + 'entity' => $answer + ]); + } + + if (empty($receiver)) { + $this->emailManager->setReceiver($defaultSender); + } else { + $this->emailManager->setReceiver($receiver); + } + + // Send the message + $this->emailManager->send(); + } +} diff --git a/src/DependencyInjection/Compiler/MediaFinderCompilerPass.php b/src/DependencyInjection/Compiler/MediaFinderCompilerPass.php index b6deacab..ee69f857 100644 --- a/src/DependencyInjection/Compiler/MediaFinderCompilerPass.php +++ b/src/DependencyInjection/Compiler/MediaFinderCompilerPass.php @@ -15,6 +15,7 @@ class MediaFinderCompilerPass implements CompilerPassInterface public function process(ContainerBuilder $container): void { if ($container->hasParameter('roadiz_core.medias.supported_platforms')) { + /** @var array $parameter */ $parameter = $container->getParameter('roadiz_core.medias.supported_platforms'); $taggedServices = $container->findTaggedServiceIds( 'roadiz_core.media_finder' diff --git a/src/DependencyInjection/Compiler/NodesSourcesEntitiesPathCompilerPass.php b/src/DependencyInjection/Compiler/NodesSourcesEntitiesPathCompilerPass.php index d771a7ca..037afe46 100644 --- a/src/DependencyInjection/Compiler/NodesSourcesEntitiesPathCompilerPass.php +++ b/src/DependencyInjection/Compiler/NodesSourcesEntitiesPathCompilerPass.php @@ -15,6 +15,9 @@ class NodesSourcesEntitiesPathCompilerPass implements CompilerPassInterface public function process(ContainerBuilder $container): void { $projectDir = $container->getParameter('kernel.project_dir'); + if (!\is_string($projectDir)) { + throw new \RuntimeException('kernel.project_dir parameter must be a string.'); + } $container->setParameter('roadiz_core.generated_entities_dir', $projectDir . '/src/GeneratedEntity'); $container->setParameter('roadiz_core.serialized_node_types_dir', $projectDir . '/src/Resources/node-types'); $container->setParameter('roadiz_core.import_files_config_path', $projectDir . '/src/Resources/config.yml'); diff --git a/src/DependencyInjection/Compiler/TreeWalkerDefinitionFactoryCompilerPass.php b/src/DependencyInjection/Compiler/TreeWalkerDefinitionFactoryCompilerPass.php new file mode 100644 index 00000000..b8c0d188 --- /dev/null +++ b/src/DependencyInjection/Compiler/TreeWalkerDefinitionFactoryCompilerPass.php @@ -0,0 +1,43 @@ +has(TreeWalkerGenerator::class)) { + $definition = $container->findDefinition(TreeWalkerGenerator::class); + $serviceIds = $container->findTaggedServiceIds( + 'roadiz_core.tree_walker_definition_factory', + ); + foreach ($serviceIds as $serviceId => $tags) { + foreach ($tags as $tag) { + if (isset($tag['classname']) && \is_string($tag['classname'])) { + /* + * TreeWalkerGenerator::addDefinitionFactoryConfiguration($classname, $serviceId, $onlyVisible = true) + */ + $definition->addMethodCall( + 'addDefinitionFactoryConfiguration', + [ + $tag['classname'], + new Reference($serviceId), + $tag['onlyVisible'] ?? true + ] + ); + } + } + } + } + } +} diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 5529c8d4..7f5e8948 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -4,6 +4,7 @@ namespace RZ\Roadiz\CoreBundle\DependencyInjection; +use RZ\Roadiz\CoreBundle\Api\Model\WebResponse; use RZ\Roadiz\CoreBundle\Controller\DefaultNodeSourceController; use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; use Symfony\Component\Config\Definition\Builder\NodeDefinition; @@ -37,15 +38,25 @@ public function getConfigTreeBuilder(): TreeBuilder ->scalarNode('maxVersionsShowed') ->defaultValue(10) ->end() + ->scalarNode('previewRequiredRoleName') + ->info('Role name required to access preview mode.') + ->defaultValue('ROLE_BACKEND_USER') + ->end() ->scalarNode('defaultNodeSourceController') ->defaultValue(DefaultNodeSourceController::class) ->end() + ->scalarNode('webResponseClass') + ->defaultValue(WebResponse::class) + ->end() ->booleanNode('useNativeJsonColumnType') ->defaultValue(true) ->end() ->booleanNode('hideRoadizVersion') ->defaultValue(false) ->end() + ->booleanNode('useGravatar') + ->defaultTrue() + ->end() ->scalarNode('documentsLibDir')->defaultValue( 'vendor/roadiz/documents/src' )->info('Relative path to Roadiz Documents lib sources from project directory.')->end() @@ -65,19 +76,6 @@ public function getConfigTreeBuilder(): TreeBuilder their node-type to avoid name conflicts with reachable nodes (pages). EOT) ->end() - ->arrayNode('security') - ->addDefaultsIfNotSet() - ->children() - ->scalarNode('private_key_dir') - ->defaultValue('%kernel.project_dir%/var/secret') - ->info('Asymmetric cryptographic key directory.') - ->end() - ->scalarNode('private_key_name') - ->defaultValue('default') - ->info('Asymmetric cryptographic key name.') - ->end() - ->end() - ->end() ->append($this->addSolrNode()) ->append($this->addInheritanceNode()) ->append($this->addReverseProxyCacheNode()) diff --git a/src/DependencyInjection/RoadizCoreExtension.php b/src/DependencyInjection/RoadizCoreExtension.php index fc150208..dce12f58 100644 --- a/src/DependencyInjection/RoadizCoreExtension.php +++ b/src/DependencyInjection/RoadizCoreExtension.php @@ -6,12 +6,9 @@ use League\CommonMark\Environment\Environment; use League\CommonMark\MarkdownConverter; -use RZ\Crypto\KeyChain\AsymmetricFilesystemKeyChain; -use RZ\Crypto\KeyChain\KeyChainInterface; use RZ\Roadiz\CoreBundle\Cache\CloudflareProxyCache; use RZ\Roadiz\CoreBundle\Cache\ReverseProxyCache; use RZ\Roadiz\CoreBundle\Cache\ReverseProxyCacheLocator; -use RZ\Roadiz\CoreBundle\Crypto\UniqueKeyEncoderFactory; use RZ\Roadiz\CoreBundle\Entity\CustomForm; use RZ\Roadiz\CoreBundle\Entity\Document; use RZ\Roadiz\CoreBundle\Entity\Node; @@ -21,6 +18,7 @@ use RZ\Roadiz\CoreBundle\Entity\NodeType; use RZ\Roadiz\CoreBundle\Entity\Translation; use RZ\Roadiz\CoreBundle\Repository\NodesSourcesRepository; +use RZ\Roadiz\CoreBundle\SearchEngine\SolariumLogger; use RZ\Roadiz\CoreBundle\Webhook\Message\GenericJsonPostMessage; use RZ\Roadiz\CoreBundle\Webhook\Message\GitlabPipelineTriggerMessage; use RZ\Roadiz\CoreBundle\Webhook\Message\NetlifyBuildHookMessage; @@ -59,21 +57,18 @@ public function load(array $configs, ContainerBuilder $container): void $container->setParameter('roadiz_core.app_namespace', $config['appNamespace']); $container->setParameter('roadiz_core.app_version', $config['appVersion']); + $container->setParameter('roadiz_core.use_gravatar', $config['useGravatar']); $container->setParameter('roadiz_core.health_check_token', $config['healthCheckToken']); $container->setParameter('roadiz_core.inheritance_type', $config['inheritance']['type']); $container->setParameter('roadiz_core.max_versions_showed', $config['maxVersionsShowed']); $container->setParameter('roadiz_core.static_domain_name', $config['staticDomainName'] ?? ''); - $container->setParameter('roadiz_core.private_key_name', $config['security']['private_key_name']); - $container->setParameter('roadiz_core.private_key_dir', $config['security']['private_key_dir']); - $container->setParameter( - 'roadiz_core.private_key_path', - $config['security']['private_key_dir'] . DIRECTORY_SEPARATOR . $config['security']['private_key_name'] - ); $container->setParameter('roadiz_core.default_node_source_controller', $config['defaultNodeSourceController']); $container->setParameter('roadiz_core.use_native_json_column_type', $config['useNativeJsonColumnType']); $container->setParameter('roadiz_core.use_typed_node_names', $config['useTypedNodeNames']); $container->setParameter('roadiz_core.hide_roadiz_version', $config['hideRoadizVersion']); $container->setParameter('roadiz_core.use_accept_language_header', $config['useAcceptLanguageHeader']); + $container->setParameter('roadiz_core.web_response_class', $config['webResponseClass']); + $container->setParameter('roadiz_core.preview_required_role_name', $config['previewRequiredRoleName']); /* * Assets config @@ -85,9 +80,11 @@ public function load(array $configs, ContainerBuilder $container): void $container->setParameter('roadiz_core.assets_processing.supports_webp', false); } + /** @var string $projectDir */ + $projectDir = $container->getParameter('kernel.project_dir'); $container->setParameter( 'roadiz_core.documents_lib_dir', - $container->getParameter('kernel.project_dir') . DIRECTORY_SEPARATOR . trim($config['documentsLibDir'], "/ \t\n\r\0\x0B") + $projectDir . DIRECTORY_SEPARATOR . trim($config['documentsLibDir'], "/ \t\n\r\0\x0B") ); /* * Media config @@ -128,31 +125,6 @@ public function load(array $configs, ContainerBuilder $container): void $this->registerReverseProxyCache($config, $container); $this->registerSolr($config, $container); $this->registerMarkdown($config, $container); - $this->registerCrypto($config, $container); - } - - private function registerCrypto(array $config, ContainerBuilder $container): void - { - $container->setDefinition( - UniqueKeyEncoderFactory::class, - (new Definition()) - ->setClass(UniqueKeyEncoderFactory::class) - ->setPublic(true) - ->setArguments([ - new Reference(KeyChainInterface::class), - $container->getParameter('roadiz_core.private_key_name') - ]) - ); - - $container->setDefinition( - KeyChainInterface::class, - (new Definition()) - ->setClass(AsymmetricFilesystemKeyChain::class) - ->setPublic(true) - ->setArguments([ - $container->getParameter('roadiz_core.private_key_dir') - ]) - ); } private function registerReverseProxyCache(array $config, ContainerBuilder $container): void @@ -258,6 +230,7 @@ private function registerSolr(array $config, ContainerBuilder $container): void } } if (count($solrEndpoints) > 0) { + $logger = new Reference(SolariumLogger::class); $container->setDefinition( 'roadiz_core.solr.client', (new Definition()) @@ -269,6 +242,7 @@ private function registerSolr(array $config, ContainerBuilder $container): void new Reference('roadiz_core.solr.adapter'), new Reference(EventDispatcherInterface::class) ]) + ->addMethodCall('registerPlugin', ['roadiz_core.solr.client.logger', $logger]) ->addMethodCall('setEndpoints', [array_map(function (string $endpointId) { return new Reference($endpointId); }, $solrEndpoints)]) @@ -286,21 +260,23 @@ private function registerMarkdown(array $config, ContainerBuilder $container): v 'noreferrer' => 'external', ] ]); + /** @var array $defaultConfig */ + $defaultConfig = $container->getParameter('roadiz_core.markdown_config_default'); $container->setParameter( 'roadiz_core.markdown_config_text_converter', - array_merge($container->getParameter('roadiz_core.markdown_config_default'), [ + array_merge($defaultConfig, [ 'html_input' => 'allow' ]) ); $container->setParameter( 'roadiz_core.markdown_config_text_extra_converter', - array_merge($container->getParameter('roadiz_core.markdown_config_default'), [ + array_merge($defaultConfig, [ 'html_input' => 'allow' ]) ); $container->setParameter( 'roadiz_core.markdown_config_line_converter', - array_merge($container->getParameter('roadiz_core.markdown_config_default'), [ + array_merge($defaultConfig, [ 'html_input' => 'escape' ]) ); diff --git a/src/Doctrine/Event/FilterQueryBuilderEvent.php b/src/Doctrine/Event/FilterQueryBuilderEvent.php index 983b74a8..202811ca 100644 --- a/src/Doctrine/Event/FilterQueryBuilderEvent.php +++ b/src/Doctrine/Event/FilterQueryBuilderEvent.php @@ -23,7 +23,7 @@ abstract class FilterQueryBuilderEvent extends Event /** * @param QueryBuilder $queryBuilder - * @param string $entityClass + * @param class-string $entityClass */ public function __construct(QueryBuilder $queryBuilder, string $entityClass) { @@ -51,10 +51,10 @@ public function setQueryBuilder(QueryBuilder $queryBuilder) /** - * @param string $entityClass + * @param class-string $entityClass * @return bool */ - public function supports($entityClass): bool + public function supports(string $entityClass): bool { return $this->entityClass === $entityClass; } diff --git a/src/Doctrine/Event/QueryEvent.php b/src/Doctrine/Event/QueryEvent.php index b164dcbd..dc85f161 100644 --- a/src/Doctrine/Event/QueryEvent.php +++ b/src/Doctrine/Event/QueryEvent.php @@ -18,7 +18,7 @@ class QueryEvent extends Event /** * @param Query $query - * @param string $entityClass + * @param class-string $entityClass */ public function __construct(Query $query, string $entityClass) { @@ -35,7 +35,7 @@ public function getQuery(): Query } /** - * @return string + * @return class-string */ public function getEntityClass(): string { diff --git a/src/Doctrine/Event/QueryNodesSourcesEvent.php b/src/Doctrine/Event/QueryNodesSourcesEvent.php index f4dc8a38..03d2c385 100644 --- a/src/Doctrine/Event/QueryNodesSourcesEvent.php +++ b/src/Doctrine/Event/QueryNodesSourcesEvent.php @@ -16,7 +16,7 @@ final class QueryNodesSourcesEvent extends QueryEvent /** * @param Query $query - * @param string $actualEntityName + * @param class-string $actualEntityName */ public function __construct(Query $query, string $actualEntityName) { @@ -25,7 +25,7 @@ public function __construct(Query $query, string $actualEntityName) } /** - * @return string + * @return class-string */ public function getActualEntityName(): string { diff --git a/src/Doctrine/EventSubscriber/AttributeValueLifeCycleSubscriber.php b/src/Doctrine/EventSubscriber/AttributeValueLifeCycleSubscriber.php index 9e0b1e5e..4e7ada54 100644 --- a/src/Doctrine/EventSubscriber/AttributeValueLifeCycleSubscriber.php +++ b/src/Doctrine/EventSubscriber/AttributeValueLifeCycleSubscriber.php @@ -32,6 +32,13 @@ public function prePersist(LifecycleEventArgs $event): void { $entity = $event->getObject(); if ($entity instanceof AttributeValueInterface) { + if ( + null !== $entity->getAttribute() && + null !== $entity->getAttribute()->getDefaultRealm() + ) { + $entity->setRealm($entity->getAttribute()->getDefaultRealm()); + } + /* * Automatically set position only if not manually set before. */ diff --git a/src/Doctrine/EventSubscriber/SettingLifeCycleSubscriber.php b/src/Doctrine/EventSubscriber/SettingLifeCycleSubscriber.php deleted file mode 100644 index 96e5a485..00000000 --- a/src/Doctrine/EventSubscriber/SettingLifeCycleSubscriber.php +++ /dev/null @@ -1,123 +0,0 @@ -uniqueKeyEncoderFactory = $uniqueKeyEncoderFactory; - $this->privateKeyName = $privateKeyName; - $this->logger = $logger; - } - - /** - * {@inheritdoc} - */ - public function getSubscribedEvents(): array - { - return [ - Events::preUpdate, - Events::postLoad - ]; - } - - /** - * @param PreUpdateEventArgs $event - * @throws InvalidKey - */ - public function preUpdate(PreUpdateEventArgs $event): void - { - $setting = $event->getObject(); - if ($setting instanceof Setting) { - if ( - $event->hasChangedField('encrypted') && - $event->getNewValue('encrypted') === false && - null !== $setting->getRawValue() - ) { - /* - * Set raw value and do not encode it if setting is not encrypted anymore. - */ - $setting->setValue($setting->getRawValue()); - } elseif ( - $event->hasChangedField('encrypted') && - $event->getNewValue('encrypted') === true && - null !== $setting->getRawValue() - ) { - /* - * Encode value for the first time. - */ - $setting->setValue($this->getEncoder()->encode(new HiddenString($setting->getRawValue()))); - } elseif ( - $setting->isEncrypted() && - $event->hasChangedField('value') && - null !== $event->getNewValue('value') - ) { - /* - * Encode setting if value has changed - */ - $event->setNewValue('value', $this->getEncoder()->encode(new HiddenString($event->getNewValue('value')))); - $setting->setClearValue($event->getNewValue('value')); - } - } - } - - /** - * @param LifecycleEventArgs $event - */ - public function postLoad(LifecycleEventArgs $event): void - { - $setting = $event->getObject(); - if ( - $setting instanceof Setting && - $setting->isEncrypted() && - null !== $setting->getRawValue() - ) { - try { - $setting->setClearValue($this->getEncoder()->decode($setting->getRawValue())->getString()); - } catch (InvalidKey $exception) { - $this->logger->error( - sprintf('Failed to decode "%s" setting value', $setting->getName()), - [ - 'exception_message' => $exception->getMessage(), - 'trace' => $exception->getTraceAsString(), - ] - ); - } catch (InvalidMessage $exception) { - $this->logger->error( - sprintf('Failed to decode "%s" setting value', $setting->getName()), - [ - 'exception_message' => $exception->getMessage(), - 'trace' => $exception->getTraceAsString(), - ] - ); - } - } - } - - /** - * @throws InvalidKey - */ - protected function getEncoder(): UniqueKeyEncoderInterface - { - return $this->uniqueKeyEncoderFactory->getEncoder($this->privateKeyName); - } -} diff --git a/src/Doctrine/EventSubscriber/UserLifeCycleSubscriber.php b/src/Doctrine/EventSubscriber/UserLifeCycleSubscriber.php index eb342cdb..0e3b98f8 100644 --- a/src/Doctrine/EventSubscriber/UserLifeCycleSubscriber.php +++ b/src/Doctrine/EventSubscriber/UserLifeCycleSubscriber.php @@ -24,27 +24,13 @@ final class UserLifeCycleSubscriber implements EventSubscriber { - private UserViewer $userViewer; - private EventDispatcherInterface $dispatcher; - private PasswordHasherFactoryInterface $passwordHasherFactory; - private LoggerInterface $logger; - - /** - * @param UserViewer $userViewer - * @param EventDispatcherInterface $dispatcher - * @param PasswordHasherFactoryInterface $passwordHasherFactory - * @param LoggerInterface $logger - */ public function __construct( - UserViewer $userViewer, - EventDispatcherInterface $dispatcher, - PasswordHasherFactoryInterface $passwordHasherFactory, - LoggerInterface $logger + private readonly UserViewer $userViewer, + private readonly EventDispatcherInterface $dispatcher, + private readonly PasswordHasherFactoryInterface $passwordHasherFactory, + private readonly LoggerInterface $logger, + private readonly bool $useGravatar ) { - $this->userViewer = $userViewer; - $this->dispatcher = $dispatcher; - $this->logger = $logger; - $this->passwordHasherFactory = $passwordHasherFactory; } /** @@ -94,9 +80,11 @@ public function preUpdate(PreUpdateEventArgs $event): void $user->setPictureUrl($url); } catch (\Exception $e) { $user->setFacebookName(''); - $user->setPictureUrl($user->getGravatarUrl()); + if ($this->useGravatar) { + $user->setPictureUrl($user->getGravatarUrl()); + } } - } else { + } elseif ($this->useGravatar) { $user->setPictureUrl($user->getGravatarUrl()); } } @@ -168,8 +156,7 @@ public function postPersist(LifecycleEventArgs $event): void /** * @param LifecycleEventArgs $event - * - * @throws \Exception + * @throws \Throwable */ public function prePersist(LifecycleEventArgs $event): void { @@ -189,8 +176,8 @@ public function prePersist(LifecycleEventArgs $event): void $user->setPasswordRequestedAt(new \DateTime()); $user->setConfirmationToken($tokenGenerator->generateToken()); - $this->userViewer->setUser($user); $this->userViewer->sendPasswordResetLink( + $user, 'loginResetPage', '@RoadizCore/email/users/welcome_user_email.html.twig', '@RoadizCore/email/users/welcome_user_email.txt.twig' @@ -202,7 +189,7 @@ public function prePersist(LifecycleEventArgs $event): void /* * Force a Gravatar image if not defined */ - if (empty($user->getPictureUrl())) { + if (empty($user->getPictureUrl()) && $this->useGravatar) { $user->setPictureUrl($user->getGravatarUrl()); } } diff --git a/src/Doctrine/ORM/Filter/ANodesFilter.php b/src/Doctrine/ORM/Filter/ANodesFilter.php index 8548be74..ebfcb49e 100644 --- a/src/Doctrine/ORM/Filter/ANodesFilter.php +++ b/src/Doctrine/ORM/Filter/ANodesFilter.php @@ -40,14 +40,6 @@ protected function getNodeJoinAlias(): string return 'a_n'; } - /** - * @return string - */ - protected function getNodeFieldJoinAlias(): string - { - return 'a_n_f'; - } - /** * @param QueryBuilderBuildEvent $event */ @@ -72,24 +64,9 @@ public function onNodeQueryBuilderBuild(QueryBuilderBuildEvent $event): void $this->getNodeJoinAlias() ); } - if (str_contains($event->getProperty(), $this->getProperty() . '.field.')) { - if ( - !$simpleQB->joinExists( - $simpleQB->getRootAlias(), - $this->getNodeFieldJoinAlias() - ) - ) { - $qb->innerJoin( - $this->getNodeJoinAlias() . '.field', - $this->getNodeFieldJoinAlias() - ); - } - $prefix = $this->getNodeFieldJoinAlias() . '.'; - $key = str_replace($this->getProperty() . '.field.', '', $event->getProperty()); - } else { - $prefix = $this->getNodeJoinAlias() . '.'; - $key = str_replace($this->getProperty() . '.', '', $event->getProperty()); - } + + $prefix = $this->getNodeJoinAlias() . '.'; + $key = str_replace($this->getProperty() . '.', '', $event->getProperty()); $qb->andWhere($simpleQB->buildExpressionWithoutBinding($event->getValue(), $prefix, $key, $baseKey)); } @@ -132,24 +109,9 @@ public function onNodesSourcesQueryBuilderBuild(QueryBuilderNodesSourcesBuildEve $this->getNodeJoinAlias() ); } - if (str_contains($event->getProperty(), 'node.' . $this->getProperty() . '.field.')) { - if ( - !$simpleQB->joinExists( - $simpleQB->getRootAlias(), - $this->getNodeFieldJoinAlias() - ) - ) { - $qb->innerJoin( - $this->getNodeJoinAlias() . '.field', - $this->getNodeFieldJoinAlias() - ); - } - $prefix = $this->getNodeFieldJoinAlias() . '.'; - $key = str_replace('node.' . $this->getProperty() . '.field.', '', $event->getProperty()); - } else { - $prefix = $this->getNodeJoinAlias() . '.'; - $key = str_replace('node.' . $this->getProperty() . '.', '', $event->getProperty()); - } + + $prefix = $this->getNodeJoinAlias() . '.'; + $key = str_replace('node.' . $this->getProperty() . '.', '', $event->getProperty()); $qb->andWhere($simpleQB->buildExpressionWithoutBinding($event->getValue(), $prefix, $key, $baseKey)); } diff --git a/src/Doctrine/ORM/Filter/BNodesFilter.php b/src/Doctrine/ORM/Filter/BNodesFilter.php index 032b9e7a..8cb6f894 100644 --- a/src/Doctrine/ORM/Filter/BNodesFilter.php +++ b/src/Doctrine/ORM/Filter/BNodesFilter.php @@ -35,12 +35,4 @@ protected function getNodeJoinAlias(): string { return 'b_n'; } - - /** - * @return string - */ - protected function getNodeFieldJoinAlias(): string - { - return 'b_n_f'; - } } diff --git a/src/Doctrine/ORM/SimpleQueryBuilder.php b/src/Doctrine/ORM/SimpleQueryBuilder.php index eed46053..90c7713b 100644 --- a/src/Doctrine/ORM/SimpleQueryBuilder.php +++ b/src/Doctrine/ORM/SimpleQueryBuilder.php @@ -55,7 +55,7 @@ public function buildExpressionWithBinding($value, string $prefix, string $key): * * @return Comparison|Func|string */ - public function buildExpressionWithoutBinding($value, string $prefix, string $key, string $baseKey = null) + public function buildExpressionWithoutBinding(mixed $value, string $prefix, string $key, string $baseKey = null) { if (\mb_strlen($prefix) > 0 && \mb_substr($prefix, -\mb_strlen('.')) !== '.') { $prefix .= '.'; @@ -127,14 +127,11 @@ public function buildExpressionWithoutBinding($value, string $prefix, string $ke if ($value instanceof PersistableInterface) { return $this->queryBuilder->expr()->eq($prefix . $key, ':' . $baseKey); } - if (isset($value)) { - return $this->queryBuilder->expr()->eq($prefix . $key, ':' . $baseKey); - } if (null === $value) { return $this->queryBuilder->expr()->isNull($prefix . $key); } - throw new \InvalidArgumentException('Value is not supported for expression.'); + return $this->queryBuilder->expr()->eq($prefix . $key, ':' . $baseKey); } /** @@ -181,14 +178,11 @@ public function bindValue(string $key, $value): QueryBuilder if ($value instanceof PersistableInterface) { return $this->queryBuilder->setParameter($key, $value->getId()); } - if (isset($value)) { - return $this->queryBuilder->setParameter($key, $value); - } if (null === $value) { return $this->queryBuilder; } - throw new \InvalidArgumentException('Value is not supported for binding.'); + return $this->queryBuilder->setParameter($key, $value); } /** diff --git a/src/Doctrine/SchemaUpdater.php b/src/Doctrine/SchemaUpdater.php index 7a7ea7a2..a176b299 100644 --- a/src/Doctrine/SchemaUpdater.php +++ b/src/Doctrine/SchemaUpdater.php @@ -98,23 +98,15 @@ public function updateSchema(): void */ public function updateNodeTypesSchema(): void { - /* - * Execute pending application migrations - */ - $this->updateSchema(); - - /* - * Update schema with new node-types - * without creating any migration - */ + $this->clearMetadata(); $process = $this->runCommand( - 'doctrine:schema:update', - '--dump-sql --force', + 'doctrine:migrations:diff', + '--namespace=DoctrineMigrations --quiet --allow-empty-diff', ); $process->run(); - if ($process->wait() === 0) { - $this->logger->info('DB schema has been updated.'); + $this->logger->info('New migration has been generated.'); + $this->updateSchema(); } else { throw new \RuntimeException('DB schema update failed. ' . $process->getErrorOutput()); } diff --git a/src/Document/EventSubscriber/DocumentMessageDispatchSubscriber.php b/src/Document/EventSubscriber/DocumentMessageDispatchSubscriber.php index 6abfdccf..8dfb1350 100644 --- a/src/Document/EventSubscriber/DocumentMessageDispatchSubscriber.php +++ b/src/Document/EventSubscriber/DocumentMessageDispatchSubscriber.php @@ -49,18 +49,19 @@ public function onFilterDocumentEvent(FilterDocumentEvent $event): void $document = $event->getDocument(); if ( $document instanceof Document && - null !== $document->getId() && + \is_numeric($document->getId()) && $document->isLocal() && null !== $document->getRelativePath() ) { - $this->bus->dispatch(new Envelope(new DocumentRawMessage($document->getId()))); - $this->bus->dispatch(new Envelope(new DocumentFilesizeMessage($document->getId()))); - $this->bus->dispatch(new Envelope(new DocumentSizeMessage($document->getId()))); - $this->bus->dispatch(new Envelope(new DocumentAverageColorMessage($document->getId()))); - $this->bus->dispatch(new Envelope(new DocumentExifMessage($document->getId()))); - $this->bus->dispatch(new Envelope(new DocumentSvgMessage($document->getId()))); - $this->bus->dispatch(new Envelope(new DocumentAudioVideoMessage($document->getId()))); - $this->bus->dispatch(new Envelope(new DocumentPdfMessage($document->getId()))); + $id = (int) $document->getId(); + $this->bus->dispatch(new Envelope(new DocumentRawMessage($id))); + $this->bus->dispatch(new Envelope(new DocumentFilesizeMessage($id))); + $this->bus->dispatch(new Envelope(new DocumentSizeMessage($id))); + $this->bus->dispatch(new Envelope(new DocumentAverageColorMessage($id))); + $this->bus->dispatch(new Envelope(new DocumentExifMessage($id))); + $this->bus->dispatch(new Envelope(new DocumentSvgMessage($id))); + $this->bus->dispatch(new Envelope(new DocumentAudioVideoMessage($id))); + $this->bus->dispatch(new Envelope(new DocumentPdfMessage($id))); } } } diff --git a/src/Document/MessageHandler/DocumentAudioVideoMessageHandler.php b/src/Document/MessageHandler/DocumentAudioVideoMessageHandler.php index a9393dc0..92830140 100644 --- a/src/Document/MessageHandler/DocumentAudioVideoMessageHandler.php +++ b/src/Document/MessageHandler/DocumentAudioVideoMessageHandler.php @@ -67,12 +67,18 @@ protected function processMessage(AbstractDocumentMessage $message, DocumentInte * This process requires document files to be locally stored! */ $videoPath = \tempnam(\sys_get_temp_dir(), 'video_'); + if (false === $videoPath) { + throw new UnrecoverableMessageHandlingException('Unable to create temporary file for video processing.'); + } \rename($videoPath, $videoPath .= $document->getFilename()); /* * Copy AV locally */ $videoPathResource = \fopen($videoPath, 'w'); + if (false === $videoPathResource) { + throw new UnrecoverableMessageHandlingException('Unable to open temporary file for video processing.'); + } \stream_copy_to_stream($this->documentsStorage->readStream($document->getMountPath()), $videoPathResource); \fclose($videoPathResource); @@ -114,6 +120,9 @@ protected function extractMediaThumbnail(DocumentInterface $document, string $lo } $thumbnailPath = \tempnam(\sys_get_temp_dir(), 'thumbnail_'); + if (false === $thumbnailPath) { + throw new UnrecoverableMessageHandlingException('Unable to create temporary file for thumbnail processing.'); + } \rename($thumbnailPath, $thumbnailPath .= '.jpg'); $process = new Process([$this->ffmpegPath, '-y', '-i', $localMediaPath, '-vframes', '1', $thumbnailPath]); diff --git a/src/Document/MessageHandler/DocumentPdfMessageHandler.php b/src/Document/MessageHandler/DocumentPdfMessageHandler.php index 1a8340e3..292de562 100644 --- a/src/Document/MessageHandler/DocumentPdfMessageHandler.php +++ b/src/Document/MessageHandler/DocumentPdfMessageHandler.php @@ -51,12 +51,18 @@ protected function processMessage(AbstractDocumentMessage $message, DocumentInte * This process requires document files to be locally stored! */ $pdfPath = \tempnam(\sys_get_temp_dir(), 'pdf_'); + if (false === $pdfPath) { + throw new UnrecoverableMessageHandlingException('Cannot create temporary file for PDF thumbnail.'); + } \rename($pdfPath, $pdfPath .= $document->getFilename()); /* * Copy AV locally */ $pdfPathResource = \fopen($pdfPath, 'w'); + if (false === $pdfPathResource) { + throw new UnrecoverableMessageHandlingException('Cannot open temporary file for PDF thumbnail.'); + } \stream_copy_to_stream($this->documentsStorage->readStream($document->getMountPath()), $pdfPathResource); \fclose($pdfPathResource); @@ -75,6 +81,9 @@ protected function extractPdfThumbnail(DocumentInterface $document, string $loca } $thumbnailPath = \tempnam(\sys_get_temp_dir(), 'thumbnail_'); + if (false === $thumbnailPath) { + throw new UnrecoverableMessageHandlingException('Cannot create temporary file for PDF thumbnail.'); + } \rename($thumbnailPath, $thumbnailPath .= $document->getFilename() . '.jpg'); try { diff --git a/src/Entity/AbstractDateTimedPositioned.php b/src/Entity/AbstractDateTimedPositioned.php new file mode 100644 index 00000000..9f33830a --- /dev/null +++ b/src/Entity/AbstractDateTimedPositioned.php @@ -0,0 +1,42 @@ + 0]), + Serializer\Type("integer"), + Serializer\Groups(["attribute", "node", "nodes_sources"]), + SymfonySerializer\Groups(["attribute", "node", "nodes_sources"]), + ApiFilter(OrderFilter::class), + Range(min: 0, max: 9999), + NotNull, + ] + protected int $weight = 0; + public function __construct() { $this->attributeTranslations = new ArrayCollection(); @@ -76,6 +109,28 @@ public function setAttributeDocuments(Collection $attributeDocuments): Attribute return $this; } + public function getDefaultRealm(): ?RealmInterface + { + return $this->defaultRealm; + } + + public function setDefaultRealm(?RealmInterface $defaultRealm): Attribute + { + $this->defaultRealm = $defaultRealm; + return $this; + } + + public function getWeight(): int + { + return $this->weight; + } + + public function setWeight(?int $weight): Attribute + { + $this->weight = $weight ?? 0; + return $this; + } + /** * @return Collection */ diff --git a/src/Entity/AttributeDocuments.php b/src/Entity/AttributeDocuments.php index fed01b4b..596a3947 100644 --- a/src/Entity/AttributeDocuments.php +++ b/src/Entity/AttributeDocuments.php @@ -32,12 +32,13 @@ class AttributeDocuments extends AbstractPositioned ORM\JoinColumn( name: "attribute_id", referencedColumnName: "id", + nullable: false, onDelete: "CASCADE" ), Serializer\Exclude(), SymfonySerializer\Ignore() ] - protected ?Attribute $attribute = null; + protected Attribute $attribute; #[ ORM\ManyToOne( @@ -49,72 +50,45 @@ class AttributeDocuments extends AbstractPositioned ORM\JoinColumn( name: "document_id", referencedColumnName: "id", + nullable: false, onDelete: "CASCADE" ), Serializer\Groups(["attribute"]), SymfonySerializer\Groups(["attribute"]), Serializer\Type(Document::class) ] - protected ?Document $document = null; + protected Document $document; - /** - * @param Attribute|null $attribute - * @param Document|null $document - */ - public function __construct(Attribute $attribute = null, Document $document = null) + public function __construct(Attribute $attribute, Document $document) { $this->document = $document; $this->attribute = $attribute; } - /** - * - */ public function __clone() { if ($this->id) { $this->id = null; - $this->attribute = null; } } - /** - * Gets the value of document. - * - * @return Document|null - */ - public function getDocument(): ?Document + public function getDocument(): Document { return $this->document; } - /** - * Sets the value of document. - * - * @param Document|null $document the document - * - * @return AttributeDocuments - */ - public function setDocument(?Document $document): AttributeDocuments + public function setDocument(Document $document): AttributeDocuments { $this->document = $document; - return $this; } - /** - * @return Attribute|null - */ - public function getAttribute(): ?Attribute + public function getAttribute(): Attribute { return $this->attribute; } - /** - * @param Attribute|null $attribute - * @return AttributeDocuments - */ - public function setAttribute(?Attribute $attribute): AttributeDocuments + public function setAttribute(Attribute $attribute): AttributeDocuments { $this->attribute = $attribute; return $this; diff --git a/src/Entity/AttributeValue.php b/src/Entity/AttributeValue.php index 59df1d87..f5df8d02 100644 --- a/src/Entity/AttributeValue.php +++ b/src/Entity/AttributeValue.php @@ -4,9 +4,9 @@ namespace RZ\Roadiz\CoreBundle\Entity; -use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter as BaseFilter; -use ApiPlatform\Core\Serializer\Filter\PropertyFilter; +use ApiPlatform\Doctrine\Orm\Filter as BaseFilter; use ApiPlatform\Metadata\ApiFilter; +use ApiPlatform\Serializer\Filter\PropertyFilter; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\ORM\Mapping as ORM; use JMS\Serializer\Annotation as Serializer; @@ -15,6 +15,7 @@ use RZ\Roadiz\CoreBundle\Model\AttributeValueInterface; use RZ\Roadiz\CoreBundle\Model\AttributeValueTrait; use RZ\Roadiz\CoreBundle\Model\AttributeValueTranslationInterface; +use RZ\Roadiz\CoreBundle\Model\RealmInterface; use RZ\Roadiz\CoreBundle\Repository\AttributeValueRepository; use Symfony\Component\Serializer\Annotation as SymfonySerializer; @@ -22,32 +23,48 @@ ORM\Entity(repositoryClass: AttributeValueRepository::class), ORM\Table(name: "attribute_values"), ORM\Index(columns: ["attribute_id", "node_id"]), + ORM\Index(columns: ["node_id", "position"], name: "idx_attribute_value_node_position"), + ORM\Index(columns: ["position"], name: "idx_attribute_value_position"), ORM\HasLifecycleCallbacks, - ApiFilter(PropertyFilter::class) + ApiFilter(PropertyFilter::class), + ApiFilter(BaseFilter\OrderFilter::class, properties: [ + "position", + ]), ] class AttributeValue extends AbstractPositioned implements AttributeValueInterface { use AttributeValueTrait; - /** - * @var Node|null - */ #[ ORM\ManyToOne(targetEntity: Node::class, inversedBy: "attributeValues"), - ORM\JoinColumn(name: "node_id", onDelete: "CASCADE"), + ORM\JoinColumn(name: "node_id", referencedColumnName: "id", nullable: false, onDelete: "CASCADE"), Serializer\Groups(["attribute_node"]), SymfonySerializer\Groups(["attribute_node"]), SymfonySerializer\MaxDepth(1), ApiFilter(BaseFilter\SearchFilter::class, properties: [ "node" => "exact", "node.id" => "exact", - "node.nodeName" => "exact" + "node.nodeName" => "exact", + "node.nodeType" => "exact", + "node.nodeType.name" => "exact" ]), ApiFilter(BaseFilter\BooleanFilter::class, properties: [ "node.visible" ]) ] - protected ?Node $node = null; + protected Node $node; + + #[ORM\ManyToOne(targetEntity: Realm::class)] + #[ORM\JoinColumn( + name: 'realm_id', + referencedColumnName: 'id', + unique: false, + nullable: true, + onDelete: 'SET NULL' + )] + #[SymfonySerializer\Ignore] + #[Serializer\Exclude] + private ?RealmInterface $realm = null; public function __construct() { @@ -65,10 +82,7 @@ public function getPosition(): float return $this->position; } - /** - * @inheritDoc - */ - public function getAttributable(): ?AttributableInterface + public function getAttributable(): Node { return $this->node; } @@ -78,33 +92,36 @@ public function getAttributable(): ?AttributableInterface */ public function setAttributable(?AttributableInterface $attributable) { - if (null === $attributable || $attributable instanceof Node) { + if ($attributable instanceof Node) { $this->node = $attributable; return $this; } throw new \InvalidArgumentException('Attributable have to be an instance of Node.'); } - /** - * @return Node|null - */ - public function getNode(): ?Node + public function getNode(): Node { return $this->node; } - /** - * @param Node|null $node - * - * @return AttributeValue - */ - public function setNode(?Node $node): AttributeValue + public function setNode(Node $node): AttributeValue { $this->node = $node; return $this; } + public function getRealm(): ?RealmInterface + { + return $this->realm; + } + + public function setRealm(?RealmInterface $realm): AttributeValue + { + $this->realm = $realm; + return $this; + } + /** * After clone method. * @@ -115,14 +132,12 @@ public function __clone() if ($this->id) { $this->id = null; $attributeValueTranslations = $this->getAttributeValueTranslations(); - if ($attributeValueTranslations !== null) { - $this->attributeValueTranslations = new ArrayCollection(); - /** @var AttributeValueTranslationInterface $attributeValueTranslation */ - foreach ($attributeValueTranslations as $attributeValueTranslation) { - $cloneAttributeValueTranslation = clone $attributeValueTranslation; - $cloneAttributeValueTranslation->setAttributeValue($this); - $this->attributeValueTranslations->add($cloneAttributeValueTranslation); - } + $this->attributeValueTranslations = new ArrayCollection(); + /** @var AttributeValueTranslationInterface $attributeValueTranslation */ + foreach ($attributeValueTranslations as $attributeValueTranslation) { + $cloneAttributeValueTranslation = clone $attributeValueTranslation; + $cloneAttributeValueTranslation->setAttributeValue($this); + $this->attributeValueTranslations->add($cloneAttributeValueTranslation); } } } diff --git a/src/Entity/CustomForm.php b/src/Entity/CustomForm.php index 3194e69b..9f91c796 100644 --- a/src/Entity/CustomForm.php +++ b/src/Entity/CustomForm.php @@ -26,19 +26,22 @@ ORM\Entity(repositoryClass: CustomFormRepository::class), ORM\Table(name: "custom_forms"), ORM\HasLifecycleCallbacks, - UniqueEntity(fields: ["name"]) + UniqueEntity(fields: ["name"]), + ORM\Index(columns: ["created_at"], name: "custom_form_created_at"), + ORM\Index(columns: ["updated_at"], name: "custom_form_updated_at"), ] class CustomForm extends AbstractDateTimed { #[ - ORM\Column(name: "color", type: "string", unique: false, nullable: true), + ORM\Column(name: "color", type: "string", length: 7, unique: false, nullable: true), Serializer\Groups(["custom_form", "nodes_sources"]), + Assert\Length(max: 7), SymfonySerializer\Ignore() ] protected ?string $color = '#000000'; #[ - ORM\Column(type: "string", unique: true), + ORM\Column(type: "string", length: 250, unique: true), Serializer\Groups(["custom_form", "nodes_sources"]), SymfonySerializer\Groups(["custom_form", "nodes_sources"]), Assert\NotNull(), @@ -49,7 +52,7 @@ class CustomForm extends AbstractDateTimed private string $name = 'Untitled'; #[ - ORM\Column(name: "display_name", type: "string"), + ORM\Column(name: "display_name", type: "string", length: 250), Serializer\Groups(["custom_form", "nodes_sources"]), SymfonySerializer\Groups(["custom_form", "nodes_sources"]), Assert\NotNull(), @@ -79,6 +82,7 @@ class CustomForm extends AbstractDateTimed ORM\Column(type: "string", length: 15, nullable: true), Serializer\Groups(["custom_form"]), SymfonySerializer\Groups(["custom_form"]), + Assert\Length(max: 15), SymfonySerializer\Ignore() ] private ?string $retentionTime = null; @@ -104,7 +108,12 @@ class CustomForm extends AbstractDateTimed * @var Collection */ #[ - ORM\OneToMany(mappedBy: "customForm", targetEntity: CustomFormField::class, cascade: ["ALL"]), + ORM\OneToMany( + mappedBy: "customForm", + targetEntity: CustomFormField::class, + cascade: ["ALL"], + orphanRemoval: true + ), ORM\OrderBy(["position" => "ASC"]), Serializer\Groups(["custom_form"]), SymfonySerializer\Groups(["custom_form"]), @@ -119,7 +128,8 @@ class CustomForm extends AbstractDateTimed ORM\OneToMany( mappedBy: "customForm", targetEntity: CustomFormAnswer::class, - cascade: ["ALL"] + cascade: ["ALL"], + orphanRemoval: true ), Serializer\Exclude, SymfonySerializer\Ignore @@ -335,7 +345,6 @@ public function removeField(CustomFormField $field): CustomForm { if ($this->getFields()->contains($field)) { $this->getFields()->removeElement($field); - $field->setCustomForm(null); } return $this; diff --git a/src/Entity/CustomFormAnswer.php b/src/Entity/CustomFormAnswer.php index 81b05aed..f73abec6 100644 --- a/src/Entity/CustomFormAnswer.php +++ b/src/Entity/CustomFormAnswer.php @@ -11,6 +11,7 @@ use RZ\Roadiz\CoreBundle\Repository\CustomFormAnswerRepository; use Symfony\Component\Serializer\Annotation as SymfonySerializer; use RZ\Roadiz\Core\AbstractEntities\AbstractEntity; +use Symfony\Component\Validator\Constraints as Assert; #[ ORM\Entity(repositoryClass: CustomFormAnswerRepository::class), @@ -22,9 +23,10 @@ class CustomFormAnswer extends AbstractEntity { #[ - ORM\Column(name: "ip", type: "string", nullable: false), + ORM\Column(name: "ip", type: "string", length: 46, nullable: false), Serializer\Groups(["custom_form_answer"]), - SymfonySerializer\Groups(["custom_form_answer"]) + SymfonySerializer\Groups(["custom_form_answer"]), + Assert\Length(max: 46) ] private string $ip = ''; @@ -42,7 +44,8 @@ class CustomFormAnswer extends AbstractEntity ORM\OneToMany( mappedBy: "customFormAnswer", targetEntity: CustomFormFieldAttribute::class, - cascade: ["ALL"] + cascade: ["ALL"], + orphanRemoval: true ), Serializer\Groups(["custom_form_answer"]), SymfonySerializer\Groups(["custom_form_answer"]) @@ -54,11 +57,11 @@ class CustomFormAnswer extends AbstractEntity targetEntity: CustomForm::class, inversedBy: "customFormAnswers" ), - ORM\JoinColumn(name: "custom_form_id", referencedColumnName: "id", onDelete: "CASCADE"), + ORM\JoinColumn(name: "custom_form_id", referencedColumnName: "id", nullable: false, onDelete: "CASCADE"), Serializer\Exclude, SymfonySerializer\Ignore ] - private ?CustomForm $customForm = null; + private CustomForm $customForm; public function __construct() { diff --git a/src/Entity/CustomFormField.php b/src/Entity/CustomFormField.php index f7b90f28..d1c6ee20 100644 --- a/src/Entity/CustomFormField.php +++ b/src/Entity/CustomFormField.php @@ -50,11 +50,11 @@ class CustomFormField extends AbstractField #[ ORM\ManyToOne(targetEntity: CustomForm::class, inversedBy: "fields"), - ORM\JoinColumn(name: "custom_form_id", referencedColumnName: "id", onDelete: "CASCADE"), + ORM\JoinColumn(name: "custom_form_id", referencedColumnName: "id", nullable: false, onDelete: "CASCADE"), Serializer\Exclude, SymfonySerializer\Ignore ] - private ?CustomForm $customForm = null; + private CustomForm $customForm; /** * @var Collection @@ -84,7 +84,7 @@ public function __construct() * * @return $this */ - public function setLabel($label) + public function setLabel($label): CustomFormField { parent::setLabel($label); $this->setName($label); @@ -92,25 +92,15 @@ public function setLabel($label) return $this; } - /** - * @return CustomForm|null - */ - public function getCustomForm(): ?CustomForm + public function getCustomForm(): CustomForm { return $this->customForm; } - /** - * @param CustomForm|null $customForm - * - * @return $this - */ - public function setCustomForm(CustomForm $customForm = null): CustomFormField + public function setCustomForm(CustomForm $customForm): CustomFormField { $this->customForm = $customForm; - if (null !== $customForm) { - $this->customForm->addField($this); - } + $this->customForm->addField($this); return $this; } @@ -162,7 +152,6 @@ public function __clone() { if ($this->id) { $this->id = null; - $this->customForm = null; $this->customFormFieldAttributes = new ArrayCollection(); } } diff --git a/src/Entity/CustomFormFieldAttribute.php b/src/Entity/CustomFormFieldAttribute.php index 63c441d1..65ec6285 100644 --- a/src/Entity/CustomFormFieldAttribute.php +++ b/src/Entity/CustomFormFieldAttribute.php @@ -24,15 +24,15 @@ class CustomFormFieldAttribute extends AbstractEntity { #[ ORM\ManyToOne(targetEntity: CustomFormAnswer::class, inversedBy: "answerFields"), - ORM\JoinColumn(name: "custom_form_answer_id", referencedColumnName: "id", onDelete: "CASCADE") + ORM\JoinColumn(name: "custom_form_answer_id", referencedColumnName: "id", nullable: false, onDelete: "CASCADE") ] - protected ?CustomFormAnswer $customFormAnswer = null; + protected CustomFormAnswer $customFormAnswer; #[ ORM\ManyToOne(targetEntity: CustomFormField::class, inversedBy: "customFormFieldAttributes"), - ORM\JoinColumn(name: "custom_form_field_id", referencedColumnName: "id", onDelete: "CASCADE") + ORM\JoinColumn(name: "custom_form_field_id", referencedColumnName: "id", nullable: false, onDelete: "CASCADE") ] - protected ?CustomFormField $customFormField = null; + protected CustomFormField $customFormField; /** * @var Collection @@ -83,23 +83,11 @@ public function setValue(?string $value): CustomFormFieldAttribute return $this; } - /** - * Gets the value of customFormAnswer. - * - * @return CustomFormAnswer|null - */ - public function getCustomFormAnswer(): ?CustomFormAnswer + public function getCustomFormAnswer(): CustomFormAnswer { return $this->customFormAnswer; } - /** - * Sets the value of customFormAnswer. - * - * @param CustomFormAnswer $customFormAnswer the custom form answer - * - * @return self - */ public function setCustomFormAnswer(CustomFormAnswer $customFormAnswer): CustomFormFieldAttribute { $this->customFormAnswer = $customFormAnswer; @@ -116,20 +104,11 @@ public function __toString(): string return $this->getValue() ?? ''; } - /** - * @return CustomFormField|null - */ - public function getCustomFormField(): ?CustomFormField + public function getCustomFormField(): CustomFormField { return $this->customFormField; } - /** - * Sets the value of customFormField. - * - * @param CustomFormField $customFormField the custom form field - * @return self - */ public function setCustomFormField(CustomFormField $customFormField): CustomFormFieldAttribute { $this->customFormField = $customFormField; diff --git a/src/Entity/Document.php b/src/Entity/Document.php index 7b7d4e46..2f8db769 100644 --- a/src/Entity/Document.php +++ b/src/Entity/Document.php @@ -4,9 +4,10 @@ namespace RZ\Roadiz\CoreBundle\Entity; -use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter as BaseFilter; -use ApiPlatform\Core\Serializer\Filter\PropertyFilter; +use ApiPlatform\Doctrine\Orm\Filter as BaseFilter; use ApiPlatform\Metadata\ApiFilter; +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Serializer\Filter\PropertyFilter; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Criteria; @@ -18,7 +19,6 @@ use RZ\Roadiz\CoreBundle\Api\Filter\CopyrightValidFilter; use RZ\Roadiz\CoreBundle\Repository\DocumentRepository; use RZ\Roadiz\Documents\Models\AdvancedDocumentInterface; -use RZ\Roadiz\Documents\Models\DisplayableInterface; use RZ\Roadiz\Documents\Models\DocumentInterface; use RZ\Roadiz\Documents\Models\DocumentTrait; use RZ\Roadiz\Documents\Models\FileHashInterface; @@ -27,6 +27,7 @@ use RZ\Roadiz\Documents\Models\TimeableInterface; use RZ\Roadiz\Utils\StringHandler; use Symfony\Component\Serializer\Annotation as SymfonySerializer; +use Symfony\Component\Validator\Constraints as Assert; /** * Documents entity represent a file on server with datetime and naming. @@ -69,7 +70,7 @@ ]), ApiFilter(CopyrightValidFilter::class) ] -class Document extends AbstractDateTimed implements AdvancedDocumentInterface, HasThumbnailInterface, TimeableInterface, DisplayableInterface, FileHashInterface +class Document extends AbstractDateTimed implements AdvancedDocumentInterface, HasThumbnailInterface, TimeableInterface, FileHashInterface { use DocumentTrait; @@ -79,6 +80,9 @@ class Document extends AbstractDateTimed implements AdvancedDocumentInterface, H #[ORM\Column(name: 'copyright_valid_since', type: 'datetime', nullable: true)] #[SymfonySerializer\Groups(['document_copyright'])] #[Serializer\Groups(['document_copyright'])] + #[ApiProperty( + description: 'Document copyright starting date', + )] protected ?\DateTime $copyrightValidSince = null; /** @@ -87,8 +91,42 @@ class Document extends AbstractDateTimed implements AdvancedDocumentInterface, H #[ORM\Column(name: 'copyright_valid_until', type: 'datetime', nullable: true)] #[SymfonySerializer\Groups(['document_copyright'])] #[Serializer\Groups(['document_copyright'])] + #[ApiProperty( + description: 'Document copyright expiry date', + )] protected ?\DateTime $copyrightValidUntil = null; + /** + * @var string|null Image crop alignment. + * + * The possible values are: + * + * top-left + * top + * top-right + * left + * center (default) + * right + * bottom-left + * bottom + * bottom-right + */ + #[ORM\Column(name: 'image_crop_alignment', type: 'string', length: 12, nullable: true)] + #[SymfonySerializer\Ignore] + #[Assert\Length(max: 12)] + #[Assert\Choice(choices: [ + 'top-left', + 'top', + 'top-right', + 'left', + 'center', + 'right', + 'bottom-left', + 'bottom', + 'bottom-right' + ])] + protected ?string $imageCropAlignment = null; + #[ORM\ManyToOne( targetEntity: Document::class, cascade: ['all'], @@ -108,30 +146,42 @@ class Document extends AbstractDateTimed implements AdvancedDocumentInterface, H ] protected bool $raw = false; - #[ORM\Column(name: 'embedId', type: 'string', unique: false, nullable: true)] + #[ORM\Column(name: 'embedId', type: 'string', length: 250, unique: false, nullable: true)] #[SymfonySerializer\Groups(['document', 'document_display', 'nodes_sources', 'tag', 'attribute'])] #[Serializer\Groups(['document', 'document_display', 'nodes_sources', 'tag', 'attribute'])] #[Serializer\Type("string")] + #[ApiProperty( + description: 'Embed ID on external platforms', + example: 'FORSwsjtQSE', + )] + #[Assert\Length(max: 250)] protected ?string $embedId = null; #[ORM\Column(name: 'file_hash', type: 'string', length: 64, unique: false, nullable: true)] #[SymfonySerializer\Ignore] #[Serializer\Exclude] #[Serializer\Type('string')] + #[Assert\Length(max: 64)] protected ?string $fileHash = null; #[ORM\Column(name: 'file_hash_algorithm', type: 'string', length: 15, unique: false, nullable: true)] #[SymfonySerializer\Ignore] #[Serializer\Exclude] #[Serializer\Type('string')] + #[Assert\Length(max: 15)] protected ?string $fileHashAlgorithm = null; #[ApiFilter(BaseFilter\SearchFilter::class, strategy: "exact")] #[ApiFilter(RoadizFilter\NotFilter::class)] - #[ORM\Column(name: 'embedPlatform', type: 'string', unique: false, nullable: true)] + #[ORM\Column(name: 'embedPlatform', type: 'string', length: 100, unique: false, nullable: true)] #[SymfonySerializer\Groups(['document', 'document_display', 'nodes_sources', 'tag', 'attribute'])] #[Serializer\Groups(['document', 'document_display', 'nodes_sources', 'tag', 'attribute'])] #[Serializer\Type('string')] + #[Assert\Length(max: 100)] + #[ApiProperty( + description: 'Embed platform name', + example: 'youtube', + )] protected ?string $embedPlatform = null; /** * @var Collection @@ -174,7 +224,6 @@ class Document extends AbstractDateTimed implements AdvancedDocumentInterface, H #[ORM\OneToMany( mappedBy: 'document', targetEntity: DocumentTranslation::class, - fetch: 'EAGER', orphanRemoval: true )] #[SymfonySerializer\Ignore] @@ -185,20 +234,26 @@ class Document extends AbstractDateTimed implements AdvancedDocumentInterface, H * @var string|null */ #[ApiFilter(BaseFilter\SearchFilter::class, strategy: "partial")] - #[ORM\Column(name: 'filename', type: 'string', nullable: true)] + #[ORM\Column(name: 'filename', type: 'string', length: 250, nullable: true)] #[SymfonySerializer\Ignore] #[Serializer\Groups(['document', 'nodes_sources', 'tag', 'attribute'])] #[Serializer\Type('string')] + #[Assert\Length(max: 250)] private ?string $filename = null; /** * @var string|null */ #[ApiFilter(BaseFilter\SearchFilter::class, strategy: "exact")] #[ApiFilter(RoadizFilter\NotFilter::class)] - #[ORM\Column(name: 'mime_type', type: 'string', nullable: true)] + #[ORM\Column(name: 'mime_type', type: 'string', length: 255, nullable: true)] #[SymfonySerializer\Groups(['document', 'document_display', 'nodes_sources', 'tag', 'attribute'])] #[Serializer\Groups(['document', 'document_display', 'nodes_sources', 'tag', 'attribute'])] #[Serializer\Type('string')] + #[Assert\Length(max: 255)] + #[ApiProperty( + description: 'Document file mime type', + example: 'image/jpeg', + )] private ?string $mimeType = null; /** * @var Collection @@ -210,8 +265,9 @@ class Document extends AbstractDateTimed implements AdvancedDocumentInterface, H /** * @var string */ - #[ORM\Column(type: 'string')] + #[ORM\Column(type: 'string', length: 100)] #[SymfonySerializer\Ignore] + #[Assert\Length(max: 100)] #[Serializer\Groups(['document', 'nodes_sources', 'tag', 'attribute'])] #[Serializer\Type('string')] private string $folder = ''; @@ -230,6 +286,10 @@ class Document extends AbstractDateTimed implements AdvancedDocumentInterface, H #[SymfonySerializer\Groups(['document', 'document_display', 'nodes_sources', 'tag', 'attribute'])] #[Serializer\Groups(['document', 'document_display', 'nodes_sources', 'tag', 'attribute'])] #[Serializer\Type('int')] + #[ApiProperty( + description: 'When document has visual size: width in pixels', + example: '1280', + )] private int $imageWidth = 0; /** * @var integer @@ -238,6 +298,10 @@ class Document extends AbstractDateTimed implements AdvancedDocumentInterface, H #[SymfonySerializer\Groups(['document', 'document_display', 'nodes_sources', 'tag', 'attribute'])] #[Serializer\Groups(['document', 'document_display', 'nodes_sources', 'tag', 'attribute'])] #[Serializer\Type('int')] + #[ApiProperty( + description: 'When document has visual size: height in pixels', + example: '800', + )] private int $imageHeight = 0; /** * @var integer @@ -246,6 +310,10 @@ class Document extends AbstractDateTimed implements AdvancedDocumentInterface, H #[SymfonySerializer\Groups(['document', 'document_display', 'nodes_sources', 'tag', 'attribute'])] #[Serializer\Groups(['document', 'document_display', 'nodes_sources', 'tag', 'attribute'])] #[Serializer\Type('int')] + #[ApiProperty( + description: 'When document is audio or video: duration in seconds', + example: '300', + )] private int $mediaDuration = 0; /** * @var string|null @@ -254,6 +322,11 @@ class Document extends AbstractDateTimed implements AdvancedDocumentInterface, H #[SymfonySerializer\Groups(['document', 'document_display', 'nodes_sources', 'tag', 'attribute'])] #[Serializer\Groups(['document', 'document_display', 'nodes_sources', 'tag', 'attribute'])] #[Serializer\Type('string')] + #[Assert\Length(max: 7)] + #[ApiProperty( + description: 'When document is image: average color in hexadecimal format', + example: '#ffffff' + )] private ?string $imageAverageColor = null; /** * @var int|null The filesize in bytes. @@ -581,6 +654,10 @@ public function setFilesize(?int $filesize): static Serializer\SerializedName("alt"), SymfonySerializer\Groups(["document", "document_display", "nodes_sources", "tag", "attribute"]), SymfonySerializer\SerializedName("alt"), + ApiProperty( + description: 'Document alternative text, for img HTML tag.', + writable: false, + ) ] public function getAlternativeText(): string { @@ -804,4 +881,15 @@ public function setEmbedId(?string $embedId): static $this->embedId = $embedId; return $this; } + + public function getImageCropAlignment(): ?string + { + return $this->imageCropAlignment; + } + + public function setImageCropAlignment(?string $imageCropAlignment): Document + { + $this->imageCropAlignment = $imageCropAlignment; + return $this; + } } diff --git a/src/Entity/DocumentTranslation.php b/src/Entity/DocumentTranslation.php index 0dd1f9db..e7ac1609 100644 --- a/src/Entity/DocumentTranslation.php +++ b/src/Entity/DocumentTranslation.php @@ -13,6 +13,7 @@ use RZ\Roadiz\CoreBundle\Repository\DocumentTranslationRepository; use RZ\Roadiz\Documents\Models\DocumentInterface; use Symfony\Component\Serializer\Annotation as SymfonySerializer; +use Symfony\Component\Validator\Constraints as Assert; #[ ORM\Entity(repositoryClass: DocumentTranslationRepository::class), @@ -22,9 +23,10 @@ ] class DocumentTranslation extends AbstractEntity implements Loggable { - #[ORM\Column(type: 'string', nullable: true)] + #[ORM\Column(type: 'string', length: 250, nullable: true)] #[SymfonySerializer\Groups(['document', 'nodes_sources', 'tag', 'attribute'])] #[Serializer\Groups(['document', 'nodes_sources', 'tag', 'attribute'])] + #[Assert\Length(max: 250)] #[Gedmo\Versioned] protected ?string $name = null; @@ -41,16 +43,16 @@ class DocumentTranslation extends AbstractEntity implements Loggable protected ?string $externalUrl = null; #[ORM\ManyToOne(targetEntity: Translation::class, fetch: 'EXTRA_LAZY', inversedBy: 'documentTranslations')] - #[ORM\JoinColumn(name: 'translation_id', referencedColumnName: 'id', onDelete: 'CASCADE')] + #[ORM\JoinColumn(name: 'translation_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')] #[SymfonySerializer\Groups(['document', 'nodes_sources', 'tag', 'attribute'])] #[Serializer\Groups(['document', 'nodes_sources', 'tag', 'attribute'])] - protected ?TranslationInterface $translation = null; + protected TranslationInterface $translation; #[ORM\ManyToOne(targetEntity: Document::class, fetch: 'EXTRA_LAZY', inversedBy: 'documentTranslations')] - #[ORM\JoinColumn(name: 'document_id', referencedColumnName: 'id', onDelete: 'CASCADE')] + #[ORM\JoinColumn(name: 'document_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')] #[SymfonySerializer\Ignore] #[Serializer\Exclude] - protected ?DocumentInterface $document; + protected DocumentInterface $document; #[ORM\Column(type: 'text', nullable: true)] #[SymfonySerializer\Groups(['document', 'nodes_sources', 'tag', 'attribute'])] @@ -77,19 +79,11 @@ public function setName(?string $name): DocumentTranslation return $this; } - /** - * @return string - */ public function getDescription(): ?string { return $this->description; } - /** - * @param string|null $description - * - * @return $this - */ public function setDescription(?string $description): DocumentTranslation { $this->description = $description; @@ -165,7 +159,7 @@ public function getDocument(): DocumentInterface * @param DocumentInterface $document * @return $this */ - public function setDocument(DocumentInterface $document) + public function setDocument(DocumentInterface $document): DocumentTranslation { $this->document = $document; return $this; diff --git a/src/Entity/FieldAwareEntityTrait.php b/src/Entity/FieldAwareEntityTrait.php new file mode 100644 index 00000000..be0611eb --- /dev/null +++ b/src/Entity/FieldAwareEntityTrait.php @@ -0,0 +1,44 @@ +fieldName; + } + + public function setFieldName(string $fieldName): self + { + $this->fieldName = $fieldName; + return $this; + } + + /** + * @deprecated Use setFieldName method instead + */ + public function setField(NodeTypeFieldInterface $field): self + { + $this->fieldName = $field->getName(); + return $this; + } + + protected function initializeFieldAwareEntityTrait(?NodeTypeFieldInterface $nodeTypeField = null): void + { + if (null === $nodeTypeField) { + return; + } + $this->fieldName = $nodeTypeField->getName(); + } +} diff --git a/src/Entity/Folder.php b/src/Entity/Folder.php index 549c6e65..bcb5834d 100644 --- a/src/Entity/Folder.php +++ b/src/Entity/Folder.php @@ -4,15 +4,14 @@ namespace RZ\Roadiz\CoreBundle\Entity; -use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter as BaseFilter; -use ApiPlatform\Core\Serializer\Filter\PropertyFilter; +use ApiPlatform\Doctrine\Orm\Filter as BaseFilter; use ApiPlatform\Metadata\ApiFilter; +use ApiPlatform\Serializer\Filter\PropertyFilter; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Criteria; use Doctrine\ORM\Mapping as ORM; use JMS\Serializer\Annotation as Serializer; -use RZ\Roadiz\Core\AbstractEntities\AbstractDateTimedPositioned; use RZ\Roadiz\Core\AbstractEntities\LeafInterface; use RZ\Roadiz\Core\AbstractEntities\LeafTrait; use RZ\Roadiz\Core\AbstractEntities\TranslationInterface; @@ -101,11 +100,12 @@ class Folder extends AbstractDateTimedPositioned implements FolderInterface, Lea nullable: false, options: ['default' => '#000000'] )] + #[Assert\Length(max: 7)] #[SymfonySerializer\Groups(['folder', 'folder_color'])] protected string $color = '#000000'; #[ApiFilter(BaseFilter\SearchFilter::class, strategy: "partial")] - #[ORM\Column(name: 'folder_name', type: 'string', unique: true, nullable: false)] + #[ORM\Column(name: 'folder_name', type: 'string', length: 250, unique: true, nullable: false)] #[Serializer\Groups(['folder', 'document_folders'])] #[SymfonySerializer\Groups(['folder', 'document_folders'])] #[SymfonySerializer\SerializedName('slug')] diff --git a/src/Entity/FolderTranslation.php b/src/Entity/FolderTranslation.php index eb7577a6..76a78b84 100644 --- a/src/Entity/FolderTranslation.php +++ b/src/Entity/FolderTranslation.php @@ -26,28 +26,24 @@ ] class FolderTranslation extends AbstractEntity { - #[ORM\Column(type: 'string')] + #[ORM\Column(type: 'string', length: 250)] #[SymfonySerializer\Groups(['folder', 'document'])] #[Serializer\Groups(['folder', 'document'])] #[Assert\Length(max: 250)] protected string $name = ''; #[ORM\ManyToOne(targetEntity: Folder::class, inversedBy: 'translatedFolders')] - #[ORM\JoinColumn(name: 'folder_id', referencedColumnName: 'id', onDelete: 'CASCADE')] + #[ORM\JoinColumn(name: 'folder_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')] #[SymfonySerializer\Ignore] #[Serializer\Exclude] - protected ?Folder $folder = null; + protected Folder $folder; #[ORM\ManyToOne(targetEntity: Translation::class, fetch: 'EXTRA_LAZY', inversedBy: 'folderTranslations')] - #[ORM\JoinColumn(name: 'translation_id', referencedColumnName: 'id', onDelete: 'CASCADE')] + #[ORM\JoinColumn(name: 'translation_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')] #[SymfonySerializer\Groups(['folder', 'document'])] #[Serializer\Groups(['folder', 'document'])] - protected ?TranslationInterface $translation = null; + protected TranslationInterface $translation; - /** - * @param Folder $original - * @param TranslationInterface $translation - */ public function __construct(Folder $original, TranslationInterface $translation) { $this->setFolder($original); @@ -67,7 +63,7 @@ public function getName(): string * @param string $name * @return $this */ - public function setName(string $name) + public function setName(string $name): FolderTranslation { $this->name = $name; return $this; diff --git a/src/Entity/Group.php b/src/Entity/Group.php index 146cefdf..5bb7efd0 100644 --- a/src/Entity/Group.php +++ b/src/Entity/Group.php @@ -24,7 +24,7 @@ ] class Group extends AbstractEntity { - #[ORM\Column(type: 'string', unique: true)] + #[ORM\Column(type: 'string', length: 250, unique: true)] #[SymfonySerializer\Groups(['user', 'role', 'group'])] #[Serializer\Groups(['user', 'role', 'group'])] #[Assert\NotBlank] diff --git a/src/Entity/LoginAttempt.php b/src/Entity/LoginAttempt.php deleted file mode 100644 index db44dc88..00000000 --- a/src/Entity/LoginAttempt.php +++ /dev/null @@ -1,106 +0,0 @@ -ipAddress = $ipAddress; - $this->username = $username; - $this->date = new \DateTimeImmutable('now'); - $this->blocksLoginUntil = new \DateTime('now'); - $this->attemptCount = 0; - } - - public function getId(): int - { - return $this->id; - } - - public function getIpAddress(): ?string - { - return $this->ipAddress; - } - - public function getDate(): \DateTimeImmutable - { - return $this->date; - } - - public function getUsername(): ?string - { - return $this->username; - } - - /** - * @return \DateTime|null - */ - public function getBlocksLoginUntil(): ?\DateTime - { - return $this->blocksLoginUntil; - } - - /** - * @param \DateTime $blocksLoginUntil - * - * @return LoginAttempt - */ - public function setBlocksLoginUntil(\DateTime $blocksLoginUntil): LoginAttempt - { - $this->blocksLoginUntil = $blocksLoginUntil; - - return $this; - } - - /** - * @return int - */ - public function getAttemptCount(): int - { - return $this->attemptCount; - } - - /** - * @return LoginAttempt - */ - public function addAttemptCount(): LoginAttempt - { - $this->attemptCount++; - return $this; - } -} diff --git a/src/Entity/Node.php b/src/Entity/Node.php index 02875828..18b73384 100644 --- a/src/Entity/Node.php +++ b/src/Entity/Node.php @@ -4,9 +4,10 @@ namespace RZ\Roadiz\CoreBundle\Entity; -use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter as BaseFilter; -use ApiPlatform\Core\Serializer\Filter\PropertyFilter; +use ApiPlatform\Doctrine\Orm\Filter as BaseFilter; use ApiPlatform\Metadata\ApiFilter; +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Serializer\Filter\PropertyFilter; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Criteria; @@ -14,8 +15,8 @@ use Gedmo\Loggable\Loggable; use Gedmo\Mapping\Annotation as Gedmo; use JMS\Serializer\Annotation as Serializer; +use RZ\Roadiz\Contracts\NodeType\NodeTypeFieldInterface; use RZ\Roadiz\Contracts\NodeType\NodeTypeInterface; -use RZ\Roadiz\Core\AbstractEntities\AbstractDateTimedPositioned; use RZ\Roadiz\Core\AbstractEntities\LeafInterface; use RZ\Roadiz\Core\AbstractEntities\LeafTrait; use RZ\Roadiz\Core\AbstractEntities\TranslationInterface; @@ -44,6 +45,7 @@ ORM\Index(columns: ["created_at"]), ORM\Index(columns: ["updated_at"]), ORM\Index(columns: ["hide_children"]), + ORM\Index(columns: ["home"]), ORM\Index(columns: ["node_name", "status"]), ORM\Index(columns: ["visible", "status"]), ORM\Index(columns: ["visible", "status", "parent_node_id"], name: "node_visible_status_parent"), @@ -51,12 +53,17 @@ ORM\Index(columns: ["nodeType_id", "status", "parent_node_id"], name: "node_nodetype_status_parent"), ORM\Index(columns: ["nodeType_id", "status", "parent_node_id", "position"], name: "node_nodetype_status_parent_position"), ORM\Index(columns: ["visible", "parent_node_id"], name: "node_visible_parent"), + ORM\Index(columns: ["parent_node_id", "position"], name: "node_parent_position"), ORM\Index(columns: ["visible", "parent_node_id", "position"], name: "node_visible_parent_position"), ORM\Index(columns: ["status", "visible", "parent_node_id", "position"], name: "node_status_visible_parent_position"), - ORM\Index(columns: ["home"]), ORM\HasLifecycleCallbacks, Gedmo\Loggable(logEntryClass: UserLogEntry::class), - UniqueEntity(fields: ["nodeName"]), + // Need to override repository method to see all nodes + UniqueEntity( + fields: 'nodeName', + message: 'nodeName.alreadyExists', + repositoryMethod: 'findOneWithoutSecurity' + ), ApiFilter(PropertyFilter::class) ] class Node extends AbstractDateTimedPositioned implements LeafInterface, AttributableInterface, Loggable @@ -80,13 +87,17 @@ class Node extends AbstractDateTimedPositioned implements LeafInterface, Attribu 'publishedAt' => 'ns.publishedAt', ]; - #[ORM\Column(name: 'node_name', type: 'string', unique: true)] + #[ORM\Column(name: 'node_name', type: 'string', length: 255, unique: true)] #[SymfonySerializer\Groups(['nodes_sources', 'nodes_sources_base', 'node', 'log_sources'])] #[Serializer\Groups(['nodes_sources', 'nodes_sources_base', 'node', 'log_sources'])] #[Serializer\Accessor(getter: "getNodeName", setter: "setNodeName")] #[Assert\NotNull] #[Assert\NotBlank] #[Assert\Length(max: 255)] + #[ApiProperty( + description: 'Unique node name (slug) used to build content URL', + example: 'this-is-a-node-name', + )] private string $nodeName = ''; #[ORM\Column(name: 'dynamic_node_name', type: 'boolean', nullable: false, options: ['default' => true])] @@ -102,6 +113,10 @@ class Node extends AbstractDateTimedPositioned implements LeafInterface, Attribu #[SymfonySerializer\Groups(['nodes_sources_base', 'nodes_sources', 'node'])] #[Serializer\Groups(['nodes_sources_base', 'nodes_sources', 'node'])] #[Gedmo\Versioned] + #[ApiProperty( + description: 'Is this node visible in website navigation?', + example: 'true', + )] private bool $visible = true; /** @@ -114,59 +129,82 @@ class Node extends AbstractDateTimedPositioned implements LeafInterface, Attribu #[ORM\Column(type: 'integer', nullable: false, options: ['default' => 0])] #[Assert\GreaterThanOrEqual(value: 0)] + #[Assert\NotNull] #[SymfonySerializer\Ignore] #[Serializer\Exclude] #[Gedmo\Versioned] - private int $ttl = 0; + // @phpstan-ignore-next-line + private ?int $ttl = 0; #[ORM\Column(type: 'boolean', nullable: false, options: ['default' => false])] #[SymfonySerializer\Groups(['node'])] #[Serializer\Groups(['node'])] #[Gedmo\Versioned] + #[ApiProperty( + description: 'Is this node locked to prevent deletion and renaming?', + example: 'false', + )] private bool $locked = false; - /** - * @var float|string|int - */ - #[ORM\Column(type: 'decimal', precision: 2, scale: 1)] - #[SymfonySerializer\Groups(['node'])] - #[Serializer\Groups(['node'])] - #[Gedmo\Versioned] - private string|float|int $priority = 0.8; - #[ORM\Column(name: 'hide_children', type: 'boolean', nullable: false, options: ['default' => false])] #[SymfonySerializer\Groups(['node'])] #[Serializer\Groups(['node'])] #[Gedmo\Versioned] + #[ApiProperty( + description: 'Does this node act as a container for other nodes?', + example: 'false', + )] private bool $hideChildren = false; #[ORM\Column(type: 'boolean', nullable: false, options: ['default' => false])] #[SymfonySerializer\Groups(['node'])] #[Serializer\Groups(['node'])] #[Gedmo\Versioned] + #[ApiProperty( + description: 'Can this node hold other nodes inside?', + example: 'false', + )] private bool $sterile = false; - #[ORM\Column(name: 'children_order', type: 'string')] - #[SymfonySerializer\Groups(['node'])] - #[Serializer\Groups(['node'])] + #[ORM\Column(name: 'children_order', type: 'string', length: 50)] + #[SymfonySerializer\Groups(['node', 'node_listing'])] + #[Serializer\Groups(['node', 'node_listing'])] + #[Assert\Length(max: 50)] #[Gedmo\Versioned] + #[ApiProperty( + description: 'This node children will be sorted by a given field', + example: 'position', + schema: [ + 'type' => 'string', + 'enum' => ['position', 'nodeName', 'createdAt', 'updatedAt', 'publishedAt'], + 'example' => 'position' + ], + )] private string $childrenOrder = 'position'; #[ORM\Column(name: 'children_order_direction', type: 'string', length: 4)] - #[SymfonySerializer\Groups(['node'])] - #[Serializer\Groups(['node'])] + #[SymfonySerializer\Groups(['node', 'node_listing'])] + #[Serializer\Groups(['node', 'node_listing'])] + #[Assert\Length(max: 4)] + #[Assert\Choice(choices: ['ASC', 'DESC'])] #[Gedmo\Versioned] + #[ApiProperty( + description: 'This node children will be sorted ascendant or descendant', + example: 'ASC', + schema: [ + 'type' => 'string', + 'enum' => ['ASC', 'DESC'], + 'example' => 'ASC' + ], + )] private string $childrenOrderDirection = 'ASC'; - /** - * @var NodeTypeInterface|null - */ #[ORM\ManyToOne(targetEntity: NodeTypeInterface::class)] - #[ORM\JoinColumn(name: 'nodeType_id', referencedColumnName: 'id', onDelete: 'CASCADE')] + #[ORM\JoinColumn(name: 'nodeType_id', referencedColumnName: 'id', nullable:false, onDelete: 'CASCADE')] #[SymfonySerializer\Groups(['node'])] #[Serializer\Groups(['node'])] #[SymfonySerializer\Ignore] - private ?NodeTypeInterface $nodeType = null; + private NodeTypeInterface $nodeType; /** * @var Node|null @@ -281,7 +319,7 @@ class Node extends AbstractDateTimedPositioned implements LeafInterface, Attribu /** * Create a new empty Node according to given node-type. */ - public function __construct(NodeTypeInterface $nodeType = null) + public function __construct() { $this->nodesTags = new ArrayCollection(); $this->children = new ArrayCollection(); @@ -291,8 +329,6 @@ public function __construct(NodeTypeInterface $nodeType = null) $this->aNodes = new ArrayCollection(); $this->bNodes = new ArrayCollection(); $this->attributeValues = new ArrayCollection(); - - $this->setNodeType($nodeType); $this->initAbstractDateTimed(); } @@ -383,15 +419,15 @@ public function setStatus(int|string $status): Node */ public function getTtl(): int { - return $this->ttl; + return $this->ttl ?? 0; } /** - * @param int $ttl + * @param int|null $ttl * * @return Node */ - public function setTtl(int $ttl): Node + public function setTtl(?int $ttl): Node { $this->ttl = $ttl; return $this; @@ -447,24 +483,6 @@ public function setLocked(bool $locked): static return $this; } - /** - * @return float|string - */ - public function getPriority() - { - return $this->priority; - } - - /** - * @param float|string $priority - * @return $this - */ - public function setPriority($priority): static - { - $this->priority = $priority; - return $this; - } - /** * @return bool */ @@ -778,10 +796,10 @@ public function addNodeSources(NodesSources $ns): static * @return Collection */ #[SymfonySerializer\Ignore] - public function getBNodesByField(NodeTypeField $field): Collection + public function getBNodesByField(NodeTypeFieldInterface $field): Collection { $criteria = Criteria::create(); - $criteria->andWhere(Criteria::expr()->eq('field', $field)); + $criteria->andWhere(Criteria::expr()->eq('fieldName', $field->getName())); $criteria->orderBy(['position' => 'ASC']); return $this->getBNodes()->matching($criteria); } @@ -802,9 +820,6 @@ public function getBNodes(): Collection */ public function setBNodes(Collection $bNodes): static { - foreach ($this->bNodes as $bNode) { - $bNode->setNodeA(null); - } $this->bNodes->clear(); foreach ($bNodes as $bNode) { if (!$this->hasBNode($bNode)) { @@ -819,7 +834,7 @@ public function hasBNode(NodesToNodes $bNode): bool return $this->getBNodes()->exists(function ($key, NodesToNodes $element) use ($bNode) { return $bNode->getNodeB()->getId() !== null && $element->getNodeB()->getId() === $bNode->getNodeB()->getId() && - $element->getField()->getId() === $bNode->getField()->getId(); + $element->getFieldName() === $bNode->getFieldName(); }); } @@ -836,15 +851,14 @@ public function addBNode(NodesToNodes $bNode): static return $this; } - public function clearBNodesForField(NodeTypeField $nodeTypeField): Node + public function clearBNodesForField(NodeTypeFieldInterface $field): Node { - $toRemoveCollection = $this->getBNodes()->filter(function (NodesToNodes $element) use ($nodeTypeField) { - return $element->getField()->getId() === $nodeTypeField->getId(); + $toRemoveCollection = $this->getBNodes()->filter(function (NodesToNodes $element) use ($field) { + return $element->getFieldName() === $field->getName(); }); /** @var NodesToNodes $toRemove */ foreach ($toRemoveCollection as $toRemove) { $this->getBNodes()->removeElement($toRemove); - $toRemove->setNodeA(null); } return $this; } @@ -887,19 +901,12 @@ public function setNodeName(string $nodeName): static return $this; } - /** - * @return NodeTypeInterface|null - */ - public function getNodeType(): ?NodeTypeInterface + public function getNodeType(): NodeTypeInterface { return $this->nodeType; } - /** - * @param NodeTypeInterface|null $nodeType - * @return $this - */ - public function setNodeType(?NodeTypeInterface $nodeType = null): static + public function setNodeType(NodeTypeInterface $nodeType): Node { $this->nodeType = $nodeType; return $this; @@ -929,11 +936,19 @@ public function setVisible(bool $visible): Node #[SymfonySerializer\Ignore] public function getOneLineSourceSummary(): string { - $text = "Source " . $this->getNodeSources()->first()->getId() . PHP_EOL; + $text = "Source " . + ( + $this->getNodeSources()->first() ? + $this->getNodeSources()->first()->getId() : + '' + ) . + PHP_EOL; foreach ($this->getNodeType()->getFields() as $field) { $getterName = $field->getGetterName(); - $text .= '[' . $field->getLabel() . ']: ' . $this->getNodeSources()->first()->$getterName() . PHP_EOL; + $text .= '[' . $field->getLabel() . ']: ' . + ($this->getNodeSources()->first() ? $this->getNodeSources()->first()->$getterName() : '') . + PHP_EOL; } return $text; @@ -984,9 +999,14 @@ public function __clone() // Get a random string after node-name. // This is for safety reasons // NodeDuplicator service will override it - $namePrefix = $this->getNodeSources()->first()->getTitle() != "" ? - $this->getNodeSources()->first()->getTitle() : - $this->nodeName; + $nodeSource = $this->getNodeSources()->first(); + if ($nodeSource !== false) { + $namePrefix = $nodeSource->getTitle() != "" ? + $nodeSource->getTitle() : + $this->nodeName; + } else { + $namePrefix = $this->nodeName; + } $this->setNodeName($namePrefix . "-" . uniqid()); $this->setCreatedAt(new \DateTime()); $this->setUpdatedAt(new \DateTime()); diff --git a/src/Entity/NodeType.php b/src/Entity/NodeType.php index 1f272b77..53ab01ff 100644 --- a/src/Entity/NodeType.php +++ b/src/Entity/NodeType.php @@ -9,7 +9,6 @@ use Doctrine\Common\Collections\Criteria; use Doctrine\ORM\Mapping as ORM; use JMS\Serializer\Annotation as Serializer; -use RZ\Roadiz\Contracts\NodeType\NodeTypeFieldInterface; use RZ\Roadiz\Contracts\NodeType\NodeTypeInterface; use RZ\Roadiz\Core\AbstractEntities\AbstractEntity; use RZ\Roadiz\CoreBundle\Form\Constraint as RoadizAssert; @@ -29,6 +28,7 @@ ORM\Index(columns: ["name"], name: "node_type_name"), ORM\Index(columns: ["visible"]), ORM\Index(columns: ["publishable"]), + ORM\Index(columns: ["attributable"]), ORM\Index(columns: ["hiding_nodes"]), ORM\Index(columns: ["hiding_non_reachable_nodes"]), ORM\Index(columns: ["reachable"]), @@ -39,14 +39,15 @@ class NodeType extends AbstractEntity implements NodeTypeInterface { #[ - ORM\Column(name: "color", type: "string", unique: false, nullable: true), + ORM\Column(name: "color", type: "string", length: 7, unique: false, nullable: true), Serializer\Groups(["node_type", "color"]), SymfonySerializer\Groups(["node_type", "color"]), - Serializer\Type("string") + Serializer\Type("string"), + Assert\Length(max: 7), ] protected ?string $color = '#000000'; #[ - ORM\Column(type: "string", unique: true), + ORM\Column(type: "string", length: 30, unique: true), Serializer\Groups(["node_type", "node"]), SymfonySerializer\Groups(["node_type", "node"]), Serializer\Type("string"), @@ -59,7 +60,7 @@ class NodeType extends AbstractEntity implements NodeTypeInterface ] private string $name = ''; #[ - ORM\Column(name: "display_name", type: "string"), + ORM\Column(name: "display_name", type: "string", length: 250), Serializer\Groups(["node_type", "node"]), SymfonySerializer\Groups(["node_type", "node"]), Serializer\Type("string"), @@ -89,6 +90,24 @@ class NodeType extends AbstractEntity implements NodeTypeInterface Serializer\Type("boolean") ] private bool $publishable = false; + + /** + * @var bool Define if this node-type produces nodes that will have attributes. + */ + #[ + ORM\Column(type: "boolean", nullable: false, options: ["default" => true]), + Serializer\Groups(["node_type"]), + SymfonySerializer\Groups(["node_type"]), + Serializer\Type("boolean") + ] + private bool $attributable = false; + #[ + ORM\Column(name: "attributable_by_weight", type: "boolean", nullable: false, options: ["default" => false]), + Serializer\Groups(["node_type"]), + SymfonySerializer\Groups(["node_type"]), + Serializer\Type("boolean") + ] + private bool $sortingAttributesByWeight = false; /** * Define if this node-type produces nodes that will be * viewable from a Controller. @@ -120,7 +139,12 @@ class NodeType extends AbstractEntity implements NodeTypeInterface * @var Collection */ #[ - ORM\OneToMany(mappedBy: "nodeType", targetEntity: NodeTypeField::class, cascade: ["persist", "merge"]), + ORM\OneToMany( + mappedBy: "nodeType", + targetEntity: NodeTypeField::class, + cascade: ["all"], + orphanRemoval: true + ), ORM\OrderBy(["position" => "ASC"]), Serializer\Groups(["node_type"]), SymfonySerializer\Groups(["node_type"]), @@ -133,9 +157,11 @@ class NodeType extends AbstractEntity implements NodeTypeInterface Serializer\Groups(["node_type"]), SymfonySerializer\Groups(["node_type"]), Serializer\Type("int"), - Assert\GreaterThanOrEqual(value: 0) + Assert\GreaterThanOrEqual(value: 0), + Assert\NotNull ] - private int $defaultTtl = 0; + // @phpstan-ignore-next-line + private ?int $defaultTtl = 0; /** * Define if this node-type title will be indexed during its parent indexation. */ @@ -328,15 +354,15 @@ public function setColor(?string $color): NodeType */ public function getDefaultTtl(): int { - return $this->defaultTtl; + return $this->defaultTtl ?? 0; } /** - * @param int $defaultTtl + * @param int|null $defaultTtl * * @return NodeType */ - public function setDefaultTtl(int $defaultTtl): NodeType + public function setDefaultTtl(?int $defaultTtl): NodeType { $this->defaultTtl = $defaultTtl; @@ -447,6 +473,7 @@ public function removeField(NodeTypeField $field): NodeType #[SymfonySerializer\Ignore] public function getSourceEntityFullQualifiedClassName(): string { + // @phpstan-ignore-next-line return static::getGeneratedEntitiesNamespace() . '\\' . $this->getSourceEntityClassName(); } @@ -519,4 +546,26 @@ public function setSearchable(bool $searchable): NodeType $this->searchable = $searchable; return $this; } + + public function isAttributable(): bool + { + return $this->attributable; + } + + public function setAttributable(bool $attributable): NodeType + { + $this->attributable = $attributable; + return $this; + } + + public function isSortingAttributesByWeight(): bool + { + return $this->sortingAttributesByWeight; + } + + public function setSortingAttributesByWeight(bool $sortingAttributesByWeight): NodeType + { + $this->sortingAttributesByWeight = $sortingAttributesByWeight; + return $this; + } } diff --git a/src/Entity/NodeTypeField.php b/src/Entity/NodeTypeField.php index dedfcb7c..07dd18bf 100644 --- a/src/Entity/NodeTypeField.php +++ b/src/Entity/NodeTypeField.php @@ -29,6 +29,7 @@ ORM\Index(columns: ["group_name"]), ORM\Index(columns: ["group_name_canonical"]), ORM\Index(columns: ["type"]), + ORM\Index(columns: ["name"], name: 'ntf_name'), ORM\Index(columns: ["universal"]), ORM\Index(columns: ["node_type_id", "position"], name: "ntf_type_position"), ORM\UniqueConstraint(columns: ["name", "node_type_id"]), @@ -39,11 +40,11 @@ class NodeTypeField extends AbstractField implements NodeTypeFieldInterface, SerializableInterface { #[ - ORM\Column(type: "string"), + ORM\Column(type: "string", length: 50), Serializer\Expose, Serializer\Groups(["node_type", "setting"]), SymfonySerializer\Groups(["node_type", "setting"]), - Assert\Length(max: 250), + Assert\Length(max: 50), Serializer\Type("string"), RoadizAssert\NonSqlReservedWord(), RoadizAssert\SimpleLatinString() @@ -74,11 +75,11 @@ class NodeTypeField extends AbstractField implements NodeTypeFieldInterface, Ser #[ ORM\ManyToOne(targetEntity: NodeType::class, inversedBy: "fields"), - ORM\JoinColumn(name: "node_type_id", onDelete: "CASCADE"), + ORM\JoinColumn(name: "node_type_id", nullable: false, onDelete: "CASCADE"), Serializer\Exclude(), SymfonySerializer\Ignore ] - private ?NodeTypeInterface $nodeType = null; + private NodeTypeInterface $nodeType; #[ Serializer\Groups(["node_type"]), @@ -145,53 +146,32 @@ class NodeTypeField extends AbstractField implements NodeTypeFieldInterface, Ser private bool $visible = true; #[ - Serializer\VirtualProperty(), - Serializer\Type("string"), - Serializer\Groups(["node_type"]), SymfonySerializer\Groups(["node_type"]) ] public function getNodeTypeName(): string { - return $this->getNodeType() ? $this->getNodeType()->getName() : ''; + return $this->getNodeType()->getName(); } - /** - * @return NodeTypeInterface|null - */ - public function getNodeType(): ?NodeTypeInterface + public function getNodeType(): NodeTypeInterface { return $this->nodeType; } - /** - * @param NodeTypeInterface|null $nodeType - * - * @return $this - */ - public function setNodeType(?NodeTypeInterface $nodeType) + public function setNodeType(NodeTypeInterface $nodeType): NodeTypeField { $this->nodeType = $nodeType; - return $this; } - /** - * @return int|null - */ public function getMinLength(): ?int { return $this->minLength; } - /** - * @param int|null $minLength - * - * @return $this - */ - public function setMinLength(?int $minLength) + public function setMinLength(?int $minLength): NodeTypeField { $this->minLength = $minLength; - return $this; } @@ -203,15 +183,9 @@ public function getMaxLength(): ?int return $this->maxLength; } - /** - * @param int|null $maxLength - * - * @return $this - */ - public function setMaxLength(?int $maxLength) + public function setMaxLength(?int $maxLength): NodeTypeField { $this->maxLength = $maxLength; - return $this; } @@ -229,7 +203,7 @@ public function isSearchable(): bool * @return string */ #[SymfonySerializer\Ignore] - public function getOneLineSummary() + public function getOneLineSummary(): string { return $this->getId() . " — " . $this->getLabel() . ' [' . $this->getName() . ']' . ' - ' . $this->getTypeName() . @@ -246,11 +220,7 @@ public function isIndexed(): bool return $this->indexed && $this->getDoctrineType() !== 'json'; } - /** - * @param bool $indexed - * @return $this - */ - public function setIndexed(bool $indexed) + public function setIndexed(bool $indexed): NodeTypeField { $this->indexed = $indexed; return $this; @@ -264,14 +234,9 @@ public function isVisible(): bool return $this->visible; } - /** - * @param bool $visible - * @return $this - */ - public function setVisible(bool $visible) + public function setVisible(bool $visible): NodeTypeField { $this->visible = $visible; - return $this; } @@ -296,7 +261,7 @@ public function getUniversal(): bool * @param bool $universal * @return NodeTypeField */ - public function setUniversal(bool $universal) + public function setUniversal(bool $universal): NodeTypeField { $this->universal = $universal; return $this; @@ -318,29 +283,18 @@ public function getExcludeFromSearch(): bool return $this->excludeFromSearch; } - /** - * @return bool - */ public function isExcludeFromSearch(): bool { return $this->getExcludeFromSearch(); } - /** - * @param bool $excludeFromSearch - * - * @return NodeTypeField - */ - public function setExcludeFromSearch(bool $excludeFromSearch) + public function setExcludeFromSearch(bool $excludeFromSearch): NodeTypeField { $this->excludeFromSearch = $excludeFromSearch; return $this; } - /** - * @return string|null - */ public function getSerializationExclusionExpression(): ?string { return $this->serializationExclusionExpression; diff --git a/src/Entity/NodesCustomForms.php b/src/Entity/NodesCustomForms.php index 97335583..8f7d4a39 100644 --- a/src/Entity/NodesCustomForms.php +++ b/src/Entity/NodesCustomForms.php @@ -18,62 +18,50 @@ ORM\Table(name: "nodes_custom_forms"), ORM\Index(columns: ["position"]), ORM\Index(columns: ["node_id", "position"], name: "customform_node_position"), - ORM\Index(columns: ["node_id", "node_type_field_id", "position"], name: "customform_node_field_position") + ORM\Index(columns: ["node_id", "field_name", "position"], name: "customform_node_field_position") ] class NodesCustomForms extends AbstractPositioned { - /** - * @var Node|null - */ + use FieldAwareEntityTrait; + #[ORM\ManyToOne(targetEntity: Node::class, fetch: 'EAGER', inversedBy: 'customForms')] - #[ORM\JoinColumn(name: 'node_id', referencedColumnName: 'id', onDelete: 'CASCADE')] - protected ?Node $node = null; + #[ORM\JoinColumn(name: 'node_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')] + protected Node $node; - /** - * @var CustomForm|null - */ #[ORM\ManyToOne(targetEntity: CustomForm::class, fetch: 'EAGER', inversedBy: 'nodes')] - #[ORM\JoinColumn(name: 'custom_form_id', referencedColumnName: 'id', onDelete: 'CASCADE')] - protected ?CustomForm $customForm = null; - - /** - * @var NodeTypeField|null - */ - #[ORM\ManyToOne(targetEntity: NodeTypeField::class)] - #[ORM\JoinColumn(name: 'node_type_field_id', referencedColumnName: 'id', onDelete: 'CASCADE')] - protected ?NodeTypeField $field = null; + #[ORM\JoinColumn(name: 'custom_form_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')] + protected CustomForm $customForm; /** * Create a new relation between a Node, a CustomForm and a NodeTypeField. * - * @param Node $node - * @param CustomForm $customForm - * @param NodeTypeFieldInterface $field NodeTypeField + * @param Node $node + * @param CustomForm $customForm + * @param NodeTypeFieldInterface|null $field NodeTypeField */ - public function __construct(Node $node, CustomForm $customForm, NodeTypeFieldInterface $field) + public function __construct(Node $node, CustomForm $customForm, ?NodeTypeFieldInterface $field = null) { if (!$field instanceof NodeTypeField) { throw new \InvalidArgumentException('NodesCustomForms only accept NodeTypeField'); } $this->node = $node; $this->customForm = $customForm; - $this->field = $field; + $this->initializeFieldAwareEntityTrait($field); } public function __clone() { if ($this->id) { $this->id = null; - $this->node = null; } } /** * Gets the value of node. * - * @return Node|null + * @return Node */ - public function getNode(): ?Node + public function getNode(): Node { return $this->node; } @@ -111,31 +99,6 @@ public function getCustomForm(): CustomForm public function setCustomForm(CustomForm $customForm): NodesCustomForms { $this->customForm = $customForm; - - return $this; - } - - /** - * Gets the value of field. - * - * @return NodeTypeField - */ - public function getField(): NodeTypeField - { - return $this->field; - } - - /** - * Sets the value of field. - * - * @param NodeTypeField $field the field - * - * @return self - */ - public function setField(NodeTypeField $field): NodesCustomForms - { - $this->field = $field; - return $this; } } diff --git a/src/Entity/NodesSources.php b/src/Entity/NodesSources.php index 50b327b5..8ebc470e 100644 --- a/src/Entity/NodesSources.php +++ b/src/Entity/NodesSources.php @@ -4,9 +4,10 @@ namespace RZ\Roadiz\CoreBundle\Entity; -use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter as BaseFilter; -use ApiPlatform\Core\Serializer\Filter\PropertyFilter; +use ApiPlatform\Doctrine\Orm\Filter as BaseFilter; use ApiPlatform\Metadata\ApiFilter; +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Serializer\Filter\PropertyFilter; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Criteria; @@ -15,13 +16,15 @@ use Gedmo\Loggable\Loggable; use Gedmo\Mapping\Annotation as Gedmo; use JMS\Serializer\Annotation as Serializer; -use RuntimeException; +use RZ\Roadiz\Contracts\NodeType\NodeTypeFieldInterface; use RZ\Roadiz\Core\AbstractEntities\AbstractEntity; use RZ\Roadiz\Core\AbstractEntities\TranslationInterface; use RZ\Roadiz\CoreBundle\Api\Filter as RoadizFilter; use RZ\Roadiz\CoreBundle\Repository\NodesSourcesRepository; +use RZ\Roadiz\Documents\Models\DocumentInterface; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Serializer\Annotation as SymfonySerializer; +use Symfony\Component\Validator\Constraints as Assert; /** * NodesSources store Node content according to a translation and a NodeType. @@ -57,15 +60,6 @@ class NodesSources extends AbstractEntity implements Loggable #[Serializer\Exclude] protected ?ObjectManager $objectManager = null; - /** - * @var Collection - */ - #[ORM\OneToMany(mappedBy: 'nodeSource', targetEntity: Log::class)] - #[ORM\OrderBy(['datetime' => 'DESC'])] - #[SymfonySerializer\Ignore] - #[Serializer\Exclude] - protected Collection $logs; - /** * @var Collection */ @@ -75,10 +69,15 @@ class NodesSources extends AbstractEntity implements Loggable protected Collection $redirections; #[ApiFilter(BaseFilter\SearchFilter::class, strategy: "partial")] - #[ORM\Column(name: 'title', type: 'string', unique: false, nullable: true)] + #[ORM\Column(name: 'title', type: 'string', length: 250, unique: false, nullable: true)] #[SymfonySerializer\Groups(['nodes_sources', 'nodes_sources_base', 'log_sources'])] #[Serializer\Groups(['nodes_sources', 'nodes_sources_base', 'log_sources'])] #[Gedmo\Versioned] + #[Assert\Length(max: 250)] + #[ApiProperty( + description: 'Content title', + example: 'This is a title', + )] protected ?string $title = null; #[ApiFilter(BaseFilter\DateFilter::class)] @@ -88,26 +87,32 @@ class NodesSources extends AbstractEntity implements Loggable #[SymfonySerializer\Groups(['nodes_sources', 'nodes_sources_base'])] #[Serializer\Groups(['nodes_sources', 'nodes_sources_base'])] #[Gedmo\Versioned] + #[ApiProperty( + description: 'Content publication date and time', + )] protected ?\DateTime $publishedAt = null; #[ApiFilter(BaseFilter\SearchFilter::class, strategy: "partial")] - #[ORM\Column(name: 'meta_title', type: 'string', unique: false)] + #[ORM\Column(name: 'meta_title', type: 'string', length: 150, unique: false)] #[SymfonySerializer\Groups(['nodes_sources'])] #[Serializer\Groups(['nodes_sources'])] #[Gedmo\Versioned] + #[Assert\Length(max: 150)] + #[ApiProperty( + description: 'Title for search engine optimization, used in HTML title tag', + example: 'This is a title', + )] protected string $metaTitle = ''; - #[ORM\Column(name: 'meta_keywords', type: 'text')] - #[SymfonySerializer\Groups(['nodes_sources'])] - #[Serializer\Groups(['nodes_sources'])] - #[Gedmo\Versioned] - protected string $metaKeywords = ''; - #[ApiFilter(BaseFilter\SearchFilter::class, strategy: "partial")] #[ORM\Column(name: 'meta_description', type: 'text')] #[SymfonySerializer\Groups(['nodes_sources'])] #[Serializer\Groups(['nodes_sources'])] #[Gedmo\Versioned] + #[ApiProperty( + description: 'Description for search engine optimization, used in HTML meta description tag', + example: 'This is a description', + )] protected string $metaDescription = ''; #[ApiFilter(BaseFilter\BooleanFilter::class)] @@ -115,6 +120,10 @@ class NodesSources extends AbstractEntity implements Loggable #[SymfonySerializer\Groups(['nodes_sources'])] #[Serializer\Groups(['nodes_sources'])] #[Gedmo\Versioned] + #[ApiProperty( + description: 'Do not allow robots to index this content, used in HTML meta robots tag', + example: 'false', + )] protected bool $noIndex = false; #[ApiFilter(BaseFilter\SearchFilter::class, properties: [ @@ -132,6 +141,12 @@ class NodesSources extends AbstractEntity implements Loggable "node.createdAt", "node.updatedAt" ])] + #[ApiFilter(BaseFilter\NumericFilter::class, properties: [ + "node.position", + ])] + #[ApiFilter(BaseFilter\RangeFilter::class, properties: [ + "node.position", + ])] #[ApiFilter(BaseFilter\DateFilter::class, properties: [ "node.createdAt", "node.updatedAt" @@ -153,20 +168,23 @@ class NodesSources extends AbstractEntity implements Loggable "node.nodesTags.tag.tagName", ])] #[ORM\ManyToOne(targetEntity: Node::class, cascade: ['persist'], fetch: 'EAGER', inversedBy: 'nodeSources')] - #[ORM\JoinColumn(name: 'node_id', referencedColumnName: 'id', onDelete: 'CASCADE')] + #[ORM\JoinColumn(name: 'node_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')] #[SymfonySerializer\Groups(['nodes_sources', 'nodes_sources_base', 'log_sources'])] #[Serializer\Groups(['nodes_sources', 'nodes_sources_base', 'log_sources'])] - private ?Node $node = null; + #[Assert\Valid] + #[Assert\NotNull] + private Node $node; #[ApiFilter(BaseFilter\SearchFilter::class, properties: [ "translation.id" => "exact", "translation.locale" => "exact", ])] #[ORM\ManyToOne(targetEntity: Translation::class, inversedBy: 'nodeSources')] - #[ORM\JoinColumn(name: 'translation_id', referencedColumnName: 'id', onDelete: 'CASCADE')] + #[ORM\JoinColumn(name: 'translation_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')] #[SymfonySerializer\Groups(['translation_base'])] #[Serializer\Groups(['translation_base'])] - private ?TranslationInterface $translation = null; + #[Assert\NotNull] + private TranslationInterface $translation; /** * @var Collection @@ -205,14 +223,10 @@ public function __construct(Node $node, TranslationInterface $translation) $this->translation = $translation; $this->urlAliases = new ArrayCollection(); $this->documentsByFields = new ArrayCollection(); - $this->logs = new ArrayCollection(); $this->redirections = new ArrayCollection(); } - /** - * @inheritDoc - * @Serializer\Exclude - */ + #[Serializer\Exclude] public function injectObjectManager(ObjectManager $objectManager): void { $this->objectManager = $objectManager; @@ -221,28 +235,18 @@ public function injectObjectManager(ObjectManager $objectManager): void #[ORM\PreUpdate] public function preUpdate(): void { - $this->getNode()?->setUpdatedAt(new \DateTime("now")); + $this->getNode()->setUpdatedAt(new \DateTime("now")); } - /** - * @return Node|null - */ - public function getNode(): ?Node + public function getNode(): Node { return $this->node; } - /** - * @param Node|null $node - * - * @return $this - */ - public function setNode(Node $node = null): NodesSources + public function setNode(Node $node): NodesSources { $this->node = $node; - if (null !== $node) { - $node->addNodeSources($this); - } + $node->addNodeSources($this); return $this; } @@ -261,17 +265,16 @@ public function addUrlAlias(UrlAlias $urlAlias): NodesSources return $this; } - public function clearDocumentsByFields(NodeTypeField $nodeTypeField): NodesSources + public function clearDocumentsByFields(NodeTypeFieldInterface $field): NodesSources { $toRemoveCollection = $this->getDocumentsByFields()->filter( - function (NodesSourcesDocuments $element) use ($nodeTypeField) { - return $element->getField()->getId() === $nodeTypeField->getId(); + function (NodesSourcesDocuments $element) use ($field) { + return $element->getFieldName() === $field->getName(); } ); /** @var NodesSourcesDocuments $toRemove */ foreach ($toRemoveCollection as $toRemove) { $this->getDocumentsByFields()->removeElement($toRemove); - $toRemove->setNodeSource(null); } return $this; @@ -285,6 +288,24 @@ public function getDocumentsByFields(): Collection return $this->documentsByFields; } + /** + * Get at least one document to represent this node-source as image. + * + * @return DocumentInterface|null + */ + #[SymfonySerializer\Ignore] + public function getOneDisplayableDocument(): ?DocumentInterface + { + return $this->getDocumentsByFields()->filter(function (NodesSourcesDocuments $nsd) { + return null !== $nsd->getDocument() && + !$nsd->getDocument()->isPrivate() && + ($nsd->getDocument()->isImage() || $nsd->getDocument()->isSvg()) && + $nsd->getDocument()->isProcessable(); + })->map(function (NodesSourcesDocuments $nsd) { + return $nsd->getDocument(); + })->first() ?: null; + } + /** * @param Collection $documentsByFields * @@ -292,9 +313,6 @@ public function getDocumentsByFields(): Collection */ public function setDocumentsByFields(Collection $documentsByFields): NodesSources { - foreach ($this->documentsByFields as $documentsByField) { - $documentsByField->setNodeSource(null); - } $this->documentsByFields->clear(); foreach ($documentsByFields as $documentsByField) { if (!$this->hasNodesSourcesDocuments($documentsByField)) { @@ -316,7 +334,7 @@ public function hasNodesSourcesDocuments(NodesSourcesDocuments $nodesSourcesDocu function ($key, NodesSourcesDocuments $element) use ($nodesSourcesDocuments) { return $nodesSourcesDocuments->getDocument()->getId() !== null && $element->getDocument()->getId() === $nodesSourcesDocuments->getDocument()->getId() && - $element->getField()->getId() === $nodesSourcesDocuments->getField()->getId(); + $element->getFieldName() === $nodesSourcesDocuments->getFieldName(); } ); } @@ -342,14 +360,14 @@ public function addDocumentsByFields(NodesSourcesDocuments $nodesSourcesDocument * * @return Document[] */ - public function getDocumentsByFieldsWithField(NodeTypeField $field): array + public function getDocumentsByFieldsWithField(NodeTypeFieldInterface $field): array { $criteria = Criteria::create(); $criteria->orderBy(['position' => 'ASC']); return $this->getDocumentsByFields() ->matching($criteria) ->filter(function (NodesSourcesDocuments $element) use ($field) { - return $element->getField() === $field; + return $element->getFieldName() === $field->getName(); }) ->map(function (NodesSourcesDocuments $nodesSourcesDocuments) { return $nodesSourcesDocuments->getDocument(); @@ -369,7 +387,7 @@ public function getDocumentsByFieldsWithName(string $fieldName): array return $this->getDocumentsByFields() ->matching($criteria) ->filter(function (NodesSourcesDocuments $element) use ($fieldName) { - return $element->getField()->getName() === $fieldName; + return $element->getFieldName() === $fieldName; }) ->map(function (NodesSourcesDocuments $nodesSourcesDocuments) { return $nodesSourcesDocuments->getDocument(); @@ -378,27 +396,6 @@ public function getDocumentsByFieldsWithName(string $fieldName): array ; } - /** - * Logs related to this node-source. - * - * @return Collection - */ - public function getLogs(): Collection - { - return $this->logs; - } - - /** - * @param Collection $logs - * @return $this - */ - public function setLogs(Collection $logs): NodesSources - { - $this->logs = $logs; - - return $this; - } - /** * @return Collection */ @@ -455,26 +452,6 @@ public function setMetaTitle(?string $metaTitle): NodesSources return $this; } - /** - * @return string - */ - public function getMetaKeywords(): string - { - return $this->metaKeywords; - } - - /** - * @param string|null $metaKeywords - * - * @return $this - */ - public function setMetaKeywords(?string $metaKeywords): NodesSources - { - $this->metaKeywords = null !== $metaKeywords ? trim($metaKeywords) : ''; - - return $this; - } - /** * @return string */ @@ -590,9 +567,6 @@ public function setTitle(?string $title): NodesSources */ public function getTranslation(): TranslationInterface { - if (null === $this->translation) { - throw new RuntimeException('Node source translation cannot be null.'); - } return $this->translation; } @@ -640,6 +614,37 @@ public function isReachable(): bool return $this->getNode()->getNodeType()->isReachable(); } + /** + * Returns current listing sort options OR parent node's if parent node is hiding children. + * + * @return array + */ + #[Serializer\Groups(['node_listing'])] + #[SymfonySerializer\Groups(['node_listing'])] + public function getListingSortOptions(): array + { + if (null !== $this->getParent() && $this->getParent()->getNode()->isHidingChildren()) { + return $this->getParent()->getListingSortOptions(); + } + return match ($this->getNode()->getChildrenOrder()) { + 'position' => [ + 'node.position' => $this->getNode()->getChildrenOrderDirection() + ], + 'nodeName' => [ + 'node.nodeName' => $this->getNode()->getChildrenOrderDirection() + ], + 'createdAt' => [ + 'node.createdAt' => $this->getNode()->getChildrenOrderDirection() + ], + 'updatedAt' => [ + 'node.updatedAt' => $this->getNode()->getChildrenOrderDirection() + ], + default => [ + 'publishedAt' => $this->getNode()->getChildrenOrderDirection() + ], + }; + } + /** * After clone method. * @@ -659,8 +664,6 @@ public function __clone() } // Clear url-aliases before cloning. $this->urlAliases->clear(); - // Clear logs before cloning. - $this->logs->clear(); } } } diff --git a/src/Entity/NodesSourcesDocuments.php b/src/Entity/NodesSourcesDocuments.php index 4e3fadc3..6842bba9 100644 --- a/src/Entity/NodesSourcesDocuments.php +++ b/src/Entity/NodesSourcesDocuments.php @@ -8,6 +8,7 @@ use RZ\Roadiz\Contracts\NodeType\NodeTypeFieldInterface; use RZ\Roadiz\Core\AbstractEntities\AbstractPositioned; use RZ\Roadiz\CoreBundle\Repository\NodesSourcesDocumentsRepository; +use Symfony\Component\Validator\Constraints as Assert; /** * Describes a complex ManyToMany relation @@ -17,13 +18,15 @@ ORM\Entity(repositoryClass: NodesSourcesDocumentsRepository::class), ORM\Table(name: "nodes_sources_documents"), ORM\Index(columns: ["position"]), - ORM\Index(columns: ["ns_id", "node_type_field_id"], name: "nsdoc_field"), - ORM\Index(columns: ["ns_id", "node_type_field_id", "position"], name: "nsdoc_field_position") + ORM\Index(columns: ["ns_id", "field_name"], name: "nsdoc_field"), + ORM\Index(columns: ["ns_id", "field_name", "position"], name: "nsdoc_field_position") ] class NodesSourcesDocuments extends AbstractPositioned { + use FieldAwareEntityTrait; + /** - * @var NodesSources|null + * @var NodesSources */ #[ORM\ManyToOne( targetEntity: NodesSources::class, @@ -31,11 +34,12 @@ class NodesSourcesDocuments extends AbstractPositioned fetch: 'EAGER', inversedBy: 'documentsByFields' )] - #[ORM\JoinColumn(name: 'ns_id', referencedColumnName: 'id', onDelete: 'CASCADE')] - protected ?NodesSources $nodeSource; + #[Assert\NotNull] + #[ORM\JoinColumn(name: 'ns_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')] + protected NodesSources $nodeSource; /** - * @var Document|null + * @var Document */ #[ORM\ManyToOne( targetEntity: Document::class, @@ -43,47 +47,40 @@ class NodesSourcesDocuments extends AbstractPositioned fetch: 'EAGER', inversedBy: 'nodesSourcesByFields' )] - #[ORM\JoinColumn(name: 'document_id', referencedColumnName: 'id', onDelete: 'CASCADE')] - protected ?Document $document; - - /** - * @var NodeTypeField|null - */ - #[ORM\ManyToOne(targetEntity: NodeTypeField::class)] - #[ORM\JoinColumn(name: 'node_type_field_id', referencedColumnName: 'id', onDelete: 'CASCADE')] - protected ?NodeTypeField $field; + #[Assert\NotNull] + #[ORM\JoinColumn(name: 'document_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')] + protected Document $document; /** * Create a new relation between NodeSource, a Document and a NodeTypeField. * * @param NodesSources $nodeSource NodesSources and inherited types - * @param Document $document Document to link - * @param NodeTypeFieldInterface $field NodeTypeField + * @param Document $document Document to link + * @param NodeTypeFieldInterface|null $field NodeTypeField */ - public function __construct(NodesSources $nodeSource, Document $document, NodeTypeFieldInterface $field) + public function __construct(NodesSources $nodeSource, Document $document, ?NodeTypeFieldInterface $field = null) { if (!$field instanceof NodeTypeField) { throw new \InvalidArgumentException('NodesSourcesDocuments field must be a NodeTypeField instance.'); } $this->nodeSource = $nodeSource; $this->document = $document; - $this->field = $field; + $this->initializeFieldAwareEntityTrait($field); } public function __clone() { if ($this->id) { $this->id = null; - $this->nodeSource = null; } } /** * Gets the value of nodeSource. * - * @return NodesSources|null + * @return NodesSources */ - public function getNodeSource(): ?NodesSources + public function getNodeSource(): NodesSources { return $this->nodeSource; } @@ -91,11 +88,11 @@ public function getNodeSource(): ?NodesSources /** * Sets the value of nodeSource. * - * @param NodesSources|null $nodeSource the node source + * @param NodesSources $nodeSource the node source * * @return self */ - public function setNodeSource(?NodesSources $nodeSource): NodesSourcesDocuments + public function setNodeSource(NodesSources $nodeSource): NodesSourcesDocuments { $this->nodeSource = $nodeSource; @@ -105,9 +102,9 @@ public function setNodeSource(?NodesSources $nodeSource): NodesSourcesDocuments /** * Gets the value of document. * - * @return Document|null + * @return Document */ - public function getDocument(): ?Document + public function getDocument(): Document { return $this->document; } @@ -115,38 +112,14 @@ public function getDocument(): ?Document /** * Sets the value of document. * - * @param Document|null $document the document + * @param Document $document the document * * @return self */ - public function setDocument(?Document $document): NodesSourcesDocuments + public function setDocument(Document $document): NodesSourcesDocuments { $this->document = $document; return $this; } - - /** - * Gets the value of field. - * - * @return NodeTypeField|null - */ - public function getField(): ?NodeTypeField - { - return $this->field; - } - - /** - * Sets the value of field. - * - * @param NodeTypeField|null $field the field - * - * @return self - */ - public function setField(?NodeTypeField $field): NodesSourcesDocuments - { - $this->field = $field; - - return $this; - } } diff --git a/src/Entity/NodesToNodes.php b/src/Entity/NodesToNodes.php index ebcb4b6c..b80836c9 100644 --- a/src/Entity/NodesToNodes.php +++ b/src/Entity/NodesToNodes.php @@ -5,6 +5,7 @@ namespace RZ\Roadiz\CoreBundle\Entity; use Doctrine\ORM\Mapping as ORM; +use RZ\Roadiz\Contracts\NodeType\NodeTypeFieldInterface; use RZ\Roadiz\Core\AbstractEntities\AbstractPositioned; use RZ\Roadiz\CoreBundle\Repository\NodesToNodesRepository; @@ -16,62 +17,43 @@ ORM\Entity(repositoryClass: NodesToNodesRepository::class), ORM\Table(name: "nodes_to_nodes"), ORM\Index(columns: ["position"]), - ORM\Index(columns: ["node_a_id", "node_type_field_id"], name: "node_a_field"), - ORM\Index(columns: ["node_a_id", "node_type_field_id", "position"], name: "node_a_field_position"), - ORM\Index(columns: ["node_b_id", "node_type_field_id"], name: "node_b_field"), - ORM\Index(columns: ["node_b_id", "node_type_field_id", "position"], name: "node_b_field_position") + ORM\Index(columns: ["node_a_id", "field_name"], name: "node_a_field"), + ORM\Index(columns: ["node_a_id", "field_name", "position"], name: "node_a_field_position"), + ORM\Index(columns: ["node_b_id", "field_name"], name: "node_b_field"), + ORM\Index(columns: ["node_b_id", "field_name", "position"], name: "node_b_field_position") ] class NodesToNodes extends AbstractPositioned { - /** - * @var Node|null - */ + use FieldAwareEntityTrait; + #[ORM\ManyToOne(targetEntity: Node::class, cascade: ['persist'], fetch: 'EAGER', inversedBy: 'bNodes')] - #[ORM\JoinColumn(name: 'node_a_id', referencedColumnName: 'id', onDelete: 'CASCADE')] - protected ?Node $nodeA; + #[ORM\JoinColumn(name: 'node_a_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')] + protected Node $nodeA; - /** - * @var Node|null - */ #[ORM\ManyToOne(targetEntity: Node::class, cascade: ['persist'], fetch: 'EAGER', inversedBy: 'aNodes')] - #[ORM\JoinColumn(name: 'node_b_id', referencedColumnName: 'id', onDelete: 'CASCADE')] - protected ?Node $nodeB; - - /** - * @var NodeTypeField|null - */ - #[ORM\ManyToOne(targetEntity: NodeTypeField::class)] - #[ORM\JoinColumn(name: 'node_type_field_id', referencedColumnName: 'id', onDelete: 'CASCADE')] - protected ?NodeTypeField $field; + #[ORM\JoinColumn(name: 'node_b_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')] + protected Node $nodeB; - /** - * Create a new relation between two Nodes and a NodeTypeField. - * - * @param Node $nodeA - * @param Node $nodeB - * @param NodeTypeField $field NodeTypeField - */ - public function __construct(Node $nodeA, Node $nodeB, NodeTypeField $field) + public function __construct(Node $nodeA, Node $nodeB, ?NodeTypeFieldInterface $field = null) { $this->nodeA = $nodeA; $this->nodeB = $nodeB; - $this->field = $field; + $this->initializeFieldAwareEntityTrait($field); } public function __clone() { if ($this->id) { $this->id = null; - $this->nodeA = null; } } /** * Gets the value of nodeA. * - * @return Node|null + * @return Node */ - public function getNodeA(): ?Node + public function getNodeA(): Node { return $this->nodeA; } @@ -79,11 +61,11 @@ public function getNodeA(): ?Node /** * Sets the value of nodeA. * - * @param Node|null $nodeA the node + * @param Node $nodeA the node * * @return self */ - public function setNodeA(?Node $nodeA): NodesToNodes + public function setNodeA(Node $nodeA): NodesToNodes { $this->nodeA = $nodeA; @@ -93,9 +75,9 @@ public function setNodeA(?Node $nodeA): NodesToNodes /** * Gets the value of nodeB. * - * @return Node|null + * @return Node */ - public function getNodeB(): ?Node + public function getNodeB(): Node { return $this->nodeB; } @@ -103,38 +85,13 @@ public function getNodeB(): ?Node /** * Sets the value of nodeB. * - * @param Node|null $nodeB the node + * @param Node $nodeB the node * * @return self */ - public function setNodeB(?Node $nodeB): NodesToNodes + public function setNodeB(Node $nodeB): NodesToNodes { $this->nodeB = $nodeB; - - return $this; - } - - /** - * Gets the value of field. - * - * @return NodeTypeField|null - */ - public function getField(): ?NodeTypeField - { - return $this->field; - } - - /** - * Sets the value of field. - * - * @param NodeTypeField|null $field the field - * - * @return self - */ - public function setField(?NodeTypeField $field): NodesToNodes - { - $this->field = $field; - return $this; } } diff --git a/src/Entity/Realm.php b/src/Entity/Realm.php index 118fdab2..9cf68e49 100644 --- a/src/Entity/Realm.php +++ b/src/Entity/Realm.php @@ -4,20 +4,19 @@ namespace RZ\Roadiz\CoreBundle\Entity; -use ApiPlatform\Core\Serializer\Filter\PropertyFilter; +use ApiPlatform\Doctrine\Orm\Filter as BaseFilter; use ApiPlatform\Metadata\ApiFilter; +use ApiPlatform\Serializer\Filter\PropertyFilter; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use JMS\Serializer\Annotation as Serializer; +use RZ\Roadiz\Core\AbstractEntities\AbstractEntity; +use RZ\Roadiz\CoreBundle\Model\RealmInterface; use RZ\Roadiz\CoreBundle\Repository\RealmRepository; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Serializer\Annotation as SymfonySerializer; -use RZ\Roadiz\Core\AbstractEntities\AbstractEntity; -use RZ\Roadiz\CoreBundle\Model\RealmInterface; -use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\String\Slugger\AsciiSlugger; -use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter as BaseFilter; use Symfony\Component\Validator\Constraints as Assert; /** @@ -44,11 +43,13 @@ class Realm extends AbstractEntity implements RealmInterface #[ORM\Column(name: 'type', type: 'string', length: 30)] #[SymfonySerializer\Groups(['get', 'realm'])] #[Serializer\Groups(['get', 'realm'])] + #[Assert\Length(max: 30)] private string $type = RealmInterface::TYPE_PLAIN_PASSWORD; #[ORM\Column(name: 'behaviour', type: 'string', length: 30, nullable: false, options: ['default' => 'none'])] #[SymfonySerializer\Groups(['get', 'realm', 'web_response'])] #[Serializer\Groups(['get', 'realm', 'web_response'])] + #[Assert\Length(max: 30)] private string $behaviour = RealmInterface::BEHAVIOUR_NONE; #[ORM\Column(name: 'name', unique: true)] @@ -64,8 +65,9 @@ class Realm extends AbstractEntity implements RealmInterface * @var string|null * @Serializer\Exclude() */ - #[ORM\Column(name: 'plain_password', unique: false, type: 'string', length: 255, nullable: true)] + #[ORM\Column(name: 'plain_password', type: 'string', length: 255, unique: false, nullable: true)] #[SymfonySerializer\Ignore] + #[Assert\Length(max: 255)] private ?string $plainPassword = null; #[ORM\ManyToOne(targetEntity: Role::class)] @@ -77,6 +79,7 @@ class Realm extends AbstractEntity implements RealmInterface #[ORM\Column(name: 'serialization_group', type: 'string', length: 200, nullable: true)] #[SymfonySerializer\Ignore] #[Serializer\Exclude] + #[Assert\Length(max: 200)] private ?string $serializationGroup = null; /** @@ -147,7 +150,7 @@ public function setSerializationGroup(?string $serializationGroup): Realm { $this->serializationGroup = null !== $serializationGroup ? (new AsciiSlugger())->slug($serializationGroup, '_')->lower()->toString() : - (new AsciiSlugger())->slug($this->getName(), '_')->lower()->toString(); + null; return $this; } diff --git a/src/Entity/RealmNode.php b/src/Entity/RealmNode.php index d51a8b69..8007a309 100644 --- a/src/Entity/RealmNode.php +++ b/src/Entity/RealmNode.php @@ -11,6 +11,7 @@ use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Serializer\Annotation as SymfonySerializer; use RZ\Roadiz\CoreBundle\Model\RealmInterface; +use Symfony\Component\Validator\Constraints as Assert; #[ ORM\Entity(repositoryClass: RealmNodeRepository::class), @@ -41,15 +42,16 @@ class RealmNode extends AbstractEntity name: 'realm_id', referencedColumnName: 'id', unique: false, - nullable: true, + nullable: false, onDelete: 'CASCADE' )] #[SymfonySerializer\Ignore] #[Serializer\Exclude] - private ?Realm $realm = null; + private Realm $realm; #[ORM\Column(name: 'inheritance_type', type: 'string', length: 10, nullable: false)] #[SymfonySerializer\Ignore] + #[Assert\Length(max: 10)] #[Serializer\Exclude] private string $inheritanceType = RealmInterface::INHERITANCE_AUTO; @@ -72,18 +74,18 @@ public function setNode(Node $node): RealmNode } /** - * @return Realm|null + * @return Realm */ - public function getRealm(): ?Realm + public function getRealm(): Realm { return $this->realm; } /** - * @param Realm|null $realm + * @param Realm $realm * @return RealmNode */ - public function setRealm(?Realm $realm): RealmNode + public function setRealm(Realm $realm): RealmNode { $this->realm = $realm; return $this; diff --git a/src/Entity/Redirection.php b/src/Entity/Redirection.php index 2428d2e9..469222ef 100644 --- a/src/Entity/Redirection.php +++ b/src/Entity/Redirection.php @@ -18,25 +18,26 @@ ORM\Entity(repositoryClass: RedirectionRepository::class), ORM\Table(name: "redirections"), ORM\HasLifecycleCallbacks, - UniqueEntity(fields: ["query"]) + UniqueEntity(fields: ["query"]), + ORM\Index(columns: ["use_count"], name: 'redirection_use_count'), + ORM\Index(columns: ["created_at"], name: "redirection_created_at"), + ORM\Index(columns: ["updated_at"], name: "redirection_updated_at"), ] class Redirection extends AbstractDateTimed { - /** - * @var string - */ #[ORM\Column(type: 'string', length: 255, unique: true)] #[Assert\NotBlank] #[Assert\Length(max: 255)] private string $query = ""; - /** - * @var string|null - */ #[ORM\Column(name: 'redirectUri', type: 'text', length: 2048, nullable: true)] #[Assert\Length(max: 2048)] private ?string $redirectUri = null; + #[ORM\Column(name: 'use_count', type: 'integer', nullable: false, options: ['default' => 0])] + #[Assert\Length(max: 2048)] + private int $useCount = 0; + /** * @var NodesSources|null */ @@ -59,12 +60,12 @@ public function getQuery(): string } /** - * @param string $query + * @param string|null $query * @return Redirection */ - public function setQuery($query): Redirection + public function setQuery(?string $query): Redirection { - $this->query = $query; + $this->query = $query ?? ''; return $this; } @@ -80,7 +81,7 @@ public function getRedirectUri(): ?string * @param string|null $redirectUri * @return Redirection */ - public function setRedirectUri($redirectUri): Redirection + public function setRedirectUri(?string $redirectUri): Redirection { $this->redirectUri = $redirectUri; return $this; @@ -140,4 +141,18 @@ public function __construct() $this->type = Response::HTTP_MOVED_PERMANENTLY; $this->initAbstractDateTimed(); } + + /** + * @return int + */ + public function getUseCount(): int + { + return $this->useCount; + } + + public function incrementUseCount(): self + { + $this->useCount++; + return $this; + } } diff --git a/src/Entity/Role.php b/src/Entity/Role.php index 593b2dc4..d1a03ca8 100644 --- a/src/Entity/Role.php +++ b/src/Entity/Role.php @@ -36,7 +36,7 @@ class Role implements PersistableInterface ] protected ?int $id = null; - #[ORM\Column(type: 'string', unique: true)] + #[ORM\Column(type: 'string', length: 250, unique: true)] #[SymfonySerializer\Groups(['user', 'role', 'group'])] #[Serializer\Groups(['user', 'role', 'group'])] #[Assert\NotNull] diff --git a/src/Entity/Setting.php b/src/Entity/Setting.php index 6fa25d8b..2670a463 100644 --- a/src/Entity/Setting.php +++ b/src/Entity/Setting.php @@ -54,7 +54,7 @@ class Setting extends AbstractEntity AbstractField::MULTIPLE_T => 'multiple-choice.type', ]; - #[ORM\Column(type: 'string', unique: true)] + #[ORM\Column(type: 'string', length: 250, unique: true)] #[SymfonySerializer\Groups(['setting', 'nodes_sources'])] #[Serializer\Groups(['setting', 'nodes_sources'])] #[Assert\NotBlank] @@ -71,29 +71,14 @@ class Setting extends AbstractEntity #[Serializer\Groups(['setting', 'nodes_sources'])] private ?string $value = null; - /** - * Holds clear setting value after value is decoded by postLoad Doctrine event. - * - * READ ONLY: Not persisted value to hold clear value if setting is encrypted. - */ - #[SymfonySerializer\Ignore] - #[Serializer\Exclude] - private ?string $clearValue = null; - #[ORM\Column(type: 'boolean', nullable: false, options: ['default' => true])] #[SymfonySerializer\Groups(['setting'])] #[Serializer\Groups(['setting'])] private bool $visible = true; - #[ORM\Column(type: 'boolean', nullable: false, options: ['default' => false])] - #[SymfonySerializer\Groups(['setting'])] - #[Serializer\Groups(['setting'])] - private bool $encrypted = false; - #[ORM\ManyToOne( targetEntity: SettingGroup::class, cascade: ['persist', 'merge'], - fetch: 'EAGER', inversedBy: 'settings' )] #[ORM\JoinColumn(name: 'setting_group_id', referencedColumnName: 'id', onDelete: 'SET NULL')] @@ -133,7 +118,7 @@ public function getName(): string * * @return $this */ - public function setName(?string $name) + public function setName(?string $name): self { $this->name = trim(\mb_strtolower($name ?? '')); $this->name = (new UnicodeString($this->name)) @@ -173,50 +158,39 @@ public function getRawValue(): ?string } /** - * Getter for setting value OR clear value, if encrypted. - * * @return string|bool|\DateTime|int|null * @throws \Exception */ #[SymfonySerializer\Ignore] - public function getValue() + public function getValue(): string|bool|\DateTime|int|null { - if ($this->isEncrypted()) { - $value = $this->clearValue; - } else { - $value = $this->value; - } - if ($this->getType() == AbstractField::BOOLEAN_T) { - return (bool) $value; + return (bool) $this->value; } - if (null !== $value) { + if (null !== $this->value) { if ($this->getType() == AbstractField::DATETIME_T) { - return new \DateTime($value); + return new \DateTime($this->value); } if ($this->getType() == AbstractField::DOCUMENTS_T) { - return (int) $value; + return (int) $this->value; } } - return $value; + return $this->value; } /** - * @param null|mixed $value + * @param mixed $value * * @return $this */ - public function setValue($value) + public function setValue(mixed $value): self { if (null === $value) { $this->value = null; - } elseif ( - ($this->getType() === AbstractField::DATETIME_T || $this->getType() === AbstractField::DATE_T) && - $value instanceof \DateTimeInterface - ) { - $this->value = $value->format('c'); + } elseif ($value instanceof \DateTimeInterface) { + $this->value = $value->format('c'); // $value is instance of \DateTime } else { $this->value = (string) $value; } @@ -224,26 +198,6 @@ public function setValue($value) return $this; } - /** - * @return bool - */ - public function isEncrypted(): bool - { - return $this->encrypted; - } - - /** - * @param bool $encrypted - * - * @return Setting - */ - public function setEncrypted(bool $encrypted): Setting - { - $this->encrypted = $encrypted; - - return $this; - } - /** * @return int */ @@ -257,27 +211,13 @@ public function getType(): int * * @return $this */ - public function setType(int $type) + public function setType(int $type): self { $this->type = $type; return $this; } - /** - * Holds clear setting value after value is decoded by postLoad Doctrine event. - * - * @param string|null $clearValue - * - * @return Setting - */ - public function setClearValue(?string $clearValue): Setting - { - $this->clearValue = $clearValue; - - return $this; - } - /** * @return boolean */ @@ -291,7 +231,7 @@ public function isVisible(): bool * * @return $this */ - public function setVisible(bool $visible) + public function setVisible(bool $visible): self { $this->visible = $visible; @@ -311,7 +251,7 @@ public function getSettingGroup(): ?SettingGroup * * @return $this */ - public function setSettingGroup(?SettingGroup $settingGroup) + public function setSettingGroup(?SettingGroup $settingGroup): self { $this->settingGroup = $settingGroup; @@ -331,7 +271,7 @@ public function getDefaultValues(): ?string * * @return Setting */ - public function setDefaultValues(?string $defaultValues) + public function setDefaultValues(?string $defaultValues): self { $this->defaultValues = $defaultValues; diff --git a/src/Entity/SettingGroup.php b/src/Entity/SettingGroup.php index 297c7d67..a8d73195 100644 --- a/src/Entity/SettingGroup.php +++ b/src/Entity/SettingGroup.php @@ -29,7 +29,7 @@ class SettingGroup extends AbstractEntity #[Serializer\Groups(['setting', 'setting_group'])] protected bool $inMenu = false; - #[ORM\Column(type: 'string', unique: true)] + #[ORM\Column(type: 'string', length: 250, unique: true)] #[SymfonySerializer\Groups(['setting', 'setting_group'])] #[Serializer\Groups(['setting', 'setting_group'])] #[Assert\NotNull] diff --git a/src/Entity/Tag.php b/src/Entity/Tag.php index f74cd0ac..fc4f8661 100644 --- a/src/Entity/Tag.php +++ b/src/Entity/Tag.php @@ -4,14 +4,14 @@ namespace RZ\Roadiz\CoreBundle\Entity; -use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter as BaseFilter; -use ApiPlatform\Core\Serializer\Filter\PropertyFilter; +use ApiPlatform\Doctrine\Orm\Filter as BaseFilter; use ApiPlatform\Metadata\ApiFilter; +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Serializer\Filter\PropertyFilter; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use JMS\Serializer\Annotation as Serializer; -use RZ\Roadiz\Core\AbstractEntities\AbstractDateTimedPositioned; use RZ\Roadiz\Core\AbstractEntities\LeafInterface; use RZ\Roadiz\Core\AbstractEntities\LeafTrait; use RZ\Roadiz\Core\AbstractEntities\TranslationInterface; @@ -61,6 +61,10 @@ class Tag extends AbstractDateTimedPositioned implements LeafInterface #[SymfonySerializer\Groups(['tag', 'tag_color', 'color'])] #[Serializer\Groups(['tag', 'tag_color', 'color'])] #[Assert\Length(max: 7)] + #[ApiProperty( + description: 'Tag color in hexadecimal format.', + example: '#ff0000', + )] protected string $color = '#000000'; /** @@ -103,7 +107,6 @@ class Tag extends AbstractDateTimedPositioned implements LeafInterface mappedBy: 'tag', targetEntity: TagTranslation::class, cascade: ['all'], - fetch: 'EAGER', orphanRemoval: true )] #[SymfonySerializer\Groups(['translated_tag'])] @@ -111,13 +114,17 @@ class Tag extends AbstractDateTimedPositioned implements LeafInterface protected Collection $translatedTags; #[ApiFilter(BaseFilter\SearchFilter::class, strategy: "partial")] - #[ORM\Column(name: 'tag_name', type: 'string', unique: true)] + #[ORM\Column(name: 'tag_name', type: 'string', length: 250, unique: true)] #[SymfonySerializer\Ignore] #[Serializer\Groups(['tag'])] #[Serializer\Accessor(getter: "getTagName", setter: "setTagName")] #[Assert\NotNull] #[Assert\NotBlank] #[Assert\Length(max: 250)] + #[ApiProperty( + description: 'Unique tag name (slug) used to build content URL or filter queries.', + example: 'this-is-a-tag-name', + )] private string $tagName = ''; #[SymfonySerializer\Ignore] @@ -128,24 +135,50 @@ class Tag extends AbstractDateTimedPositioned implements LeafInterface #[ORM\Column(type: 'boolean', nullable: false, options: ['default' => true])] #[SymfonySerializer\Groups(['tag', 'tag_base', 'node', 'nodes_sources'])] #[Serializer\Groups(['tag', 'tag_base', 'node', 'nodes_sources'])] + #[ApiProperty( + description: 'Is this tag visible in website?', + example: 'true', + )] private bool $visible = true; - #[ORM\Column(name: 'children_order', type: 'string', options: ['default' => 'position'])] + #[ORM\Column(name: 'children_order', type: 'string', length: 60, options: ['default' => 'position'])] #[SymfonySerializer\Ignore] #[Serializer\Groups(["tag", "tag_children_order"])] #[Assert\Length(max: 60)] + #[ApiProperty( + description: 'This tag children will be sorted by a given field', + example: 'position', + schema: [ + 'type' => 'string', + 'enum' => ['position', 'tagName', 'createdAt', 'updatedAt', 'publishedAt'], + 'example' => 'position' + ], + )] private string $childrenOrder = 'position'; #[ORM\Column(name: 'children_order_direction', type: 'string', length: 4, options: ['default' => 'ASC'])] #[SymfonySerializer\Ignore] #[Serializer\Groups(["tag", "tag_children_order"])] #[Assert\Length(max: 4)] + #[ApiProperty( + description: 'This tag children will be sorted ascendant or descendant', + example: 'ASC', + schema: [ + 'type' => 'string', + 'enum' => ['ASC', 'DESC'], + 'example' => 'ASC' + ], + )] private string $childrenOrderDirection = 'ASC'; #[ApiFilter(BaseFilter\BooleanFilter::class)] #[ORM\Column(type: 'boolean', nullable: false, options: ['default' => false])] #[SymfonySerializer\Ignore] #[Serializer\Groups(["tag"])] + #[ApiProperty( + description: 'Is this tag locked to prevent deletion and renaming?', + example: 'false', + )] private bool $locked = false; /** @@ -469,4 +502,17 @@ public function setParent(?LeafInterface $parent = null): static return $this; } + + + #[ApiProperty( + description: 'Unique tag name (slug) used to build content URL or filter queries.', + example: 'this-is-a-tag-name', + )] + #[SymfonySerializer\SerializedName('slug')] + #[SymfonySerializer\Groups(['tag', 'tag_base', 'node', 'nodes_sources'])] + #[Serializer\Groups(['tag', 'tag_base', 'node', 'nodes_sources'])] + public function getSlug(): string + { + return $this->getTagName(); + } } diff --git a/src/Entity/TagTranslation.php b/src/Entity/TagTranslation.php index a93b9f45..36017fe1 100644 --- a/src/Entity/TagTranslation.php +++ b/src/Entity/TagTranslation.php @@ -30,7 +30,7 @@ ] class TagTranslation extends AbstractEntity { - #[ORM\Column(type: 'string')] + #[ORM\Column(type: 'string', length: 250)] #[SymfonySerializer\Groups(['tag', 'node', 'nodes_sources'])] #[Serializer\Groups(['tag', 'node', 'nodes_sources'])] #[Assert\NotBlank] @@ -45,16 +45,16 @@ class TagTranslation extends AbstractEntity protected ?string $description = null; #[ORM\ManyToOne(targetEntity: Tag::class, inversedBy: 'translatedTags')] - #[ORM\JoinColumn(name: 'tag_id', referencedColumnName: 'id', onDelete: 'CASCADE')] + #[ORM\JoinColumn(name: 'tag_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')] #[SymfonySerializer\Ignore] #[Serializer\Exclude] - protected ?Tag $tag = null; + protected Tag $tag; #[ORM\ManyToOne(targetEntity: Translation::class, fetch: 'EXTRA_LAZY', inversedBy: 'tagTranslations')] - #[ORM\JoinColumn(name: 'translation_id', referencedColumnName: 'id', onDelete: 'CASCADE')] + #[ORM\JoinColumn(name: 'translation_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')] #[SymfonySerializer\Groups(['tag', 'node', 'nodes_sources'])] #[Serializer\Groups(['tag', 'node', 'nodes_sources'])] - protected ?TranslationInterface $translation = null; + protected TranslationInterface $translation; /** * @var Collection @@ -73,33 +73,22 @@ class TagTranslation extends AbstractEntity /** * Create a new TagTranslation with its origin Tag and Translation. * - * @param Tag|null $original - * @param TranslationInterface|null $translation + * @param Tag $original + * @param TranslationInterface $translation */ - public function __construct(Tag $original = null, TranslationInterface $translation = null) + public function __construct(Tag $original, TranslationInterface $translation) { $this->setTag($original); $this->setTranslation($translation); $this->tagTranslationDocuments = new ArrayCollection(); - - if (null !== $original) { - $this->name = $original->getDirtyTagName() != '' ? $original->getDirtyTagName() : $original->getTagName(); - } + $this->name = $original->getDirtyTagName() != '' ? $original->getDirtyTagName() : $original->getTagName(); } - /** - * @return string - */ public function getName(): string { return $this->name; } - /** - * @param string|null $name - * - * @return $this - */ public function setName(?string $name): TagTranslation { $this->name = $name ?? ''; @@ -107,19 +96,11 @@ public function setName(?string $name): TagTranslation return $this; } - /** - * @return string - */ public function getDescription(): ?string { return $this->description; } - /** - * @param string|null $description - * - * @return $this - */ public function setDescription(?string $description): TagTranslation { $this->description = $description; @@ -127,48 +108,23 @@ public function setDescription(?string $description): TagTranslation return $this; } - /** - * Gets the value of tag. - * - * @return Tag - */ - public function getTag(): ?Tag + public function getTag(): Tag { return $this->tag; } - /** - * Sets the value of tag. - * - * @param Tag|null $tag the tag - * - * @return self - */ - public function setTag(?Tag $tag): TagTranslation + public function setTag(Tag $tag): TagTranslation { $this->tag = $tag; - return $this; } - /** - * Gets the value of translation. - * - * @return TranslationInterface|null - */ - public function getTranslation(): ?TranslationInterface + public function getTranslation(): TranslationInterface { return $this->translation; } - /** - * Sets the value of translation. - * - * @param TranslationInterface|null $translation the translation - * - * @return self - */ - public function setTranslation(?TranslationInterface $translation): TagTranslation + public function setTranslation(TranslationInterface $translation): TagTranslation { $this->translation = $translation; @@ -186,14 +142,12 @@ public function __clone() if ($this->id) { $this->id = null; $documents = $this->getDocuments(); - if ($documents !== null) { - $this->tagTranslationDocuments = new ArrayCollection(); - /** @var TagTranslationDocuments $document */ - foreach ($documents as $document) { - $cloneDocument = clone $document; - $this->tagTranslationDocuments->add($cloneDocument); - $cloneDocument->setTagTranslation($this); - } + $this->tagTranslationDocuments = new ArrayCollection(); + /** @var TagTranslationDocuments $document */ + foreach ($documents as $document) { + $cloneDocument = clone $document; + $this->tagTranslationDocuments->add($cloneDocument); + $cloneDocument->setTagTranslation($this); } } } diff --git a/src/Entity/TagTranslationDocuments.php b/src/Entity/TagTranslationDocuments.php index ca765fb0..d06683da 100644 --- a/src/Entity/TagTranslationDocuments.php +++ b/src/Entity/TagTranslationDocuments.php @@ -28,10 +28,10 @@ class TagTranslationDocuments extends AbstractPositioned fetch: 'EAGER', inversedBy: 'tagTranslationDocuments' )] - #[ORM\JoinColumn(name: 'tag_translation_id', referencedColumnName: 'id', onDelete: 'CASCADE')] + #[ORM\JoinColumn(name: 'tag_translation_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')] #[SymfonySerializer\Ignore] #[Serializer\Exclude] - protected ?TagTranslation $tagTranslation = null; + protected TagTranslation $tagTranslation; #[ORM\ManyToOne( targetEntity: Document::class, @@ -39,18 +39,18 @@ class TagTranslationDocuments extends AbstractPositioned fetch: 'EAGER', inversedBy: 'tagTranslations' )] - #[ORM\JoinColumn(name: 'document_id', referencedColumnName: 'id', onDelete: 'CASCADE')] + #[ORM\JoinColumn(name: 'document_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')] #[SymfonySerializer\Groups(['tag'])] #[Serializer\Groups(['tag'])] - protected ?Document $document = null; + protected Document $document; /** * Create a new relation between NodeSource, a Document and a NodeTypeField. * - * @param TagTranslation|null $tagTranslation - * @param Document|null $document + * @param TagTranslation $tagTranslation + * @param Document $document */ - public function __construct(TagTranslation $tagTranslation = null, Document $document = null) + public function __construct(TagTranslation $tagTranslation, Document $document) { $this->document = $document; $this->tagTranslation = $tagTranslation; @@ -60,44 +60,26 @@ public function __clone() { if ($this->id) { $this->id = null; - $this->tagTranslation = null; } } - /** - * Gets the value of document. - * - * @return Document|null - */ - public function getDocument(): ?Document + public function getDocument(): Document { return $this->document; } - /** - * Sets the value of document. - * - * @param Document|null $document the document - * - * @return self - */ - public function setDocument(?Document $document): TagTranslationDocuments + public function setDocument(Document $document): TagTranslationDocuments { $this->document = $document; - return $this; } - public function getTagTranslation(): ?TagTranslation + public function getTagTranslation(): TagTranslation { return $this->tagTranslation; } - /** - * @param TagTranslation|null $tagTranslation - * @return TagTranslationDocuments - */ - public function setTagTranslation(?TagTranslation $tagTranslation): TagTranslationDocuments + public function setTagTranslation(TagTranslation $tagTranslation): TagTranslationDocuments { $this->tagTranslation = $tagTranslation; return $this; diff --git a/src/Entity/Theme.php b/src/Entity/Theme.php index 86807262..57d5afd6 100644 --- a/src/Entity/Theme.php +++ b/src/Entity/Theme.php @@ -87,15 +87,16 @@ public function getInformations(): array $class = $this->getClassName(); if (class_exists($class)) { - $reflector = new \ReflectionClass($class); - if ($reflector->isSubclassOf('\\RZ\\Roadiz\\CMS\\Controllers\\AppController')) { - return [ - 'name' => call_user_func([$class, 'getThemeName']), - 'author' => call_user_func([$class, 'getThemeAuthor']), - 'copyright' => call_user_func([$class, 'getThemeCopyright']), - 'dir' => call_user_func([$class, 'getThemeDir']) - ]; - } + $nameCallable = [$class, 'getThemeName']; + $authorCallable = [$class, 'getThemeAuthor']; + $copyrightCallable = [$class, 'getThemeCopyright']; + $dirCallable = [$class, 'getThemeDir']; + return [ + 'name' => \is_callable($nameCallable) ? call_user_func($nameCallable) : null, + 'author' => \is_callable($authorCallable) ? call_user_func($authorCallable) : null, + 'copyright' => \is_callable($copyrightCallable) ? call_user_func($copyrightCallable) : null, + 'dir' => \is_callable($dirCallable) ? call_user_func($dirCallable) : null, + ]; } return []; diff --git a/src/Entity/Translation.php b/src/Entity/Translation.php index d585fed0..549360e8 100644 --- a/src/Entity/Translation.php +++ b/src/Entity/Translation.php @@ -4,9 +4,10 @@ namespace RZ\Roadiz\CoreBundle\Entity; -use ApiPlatform\Core\Serializer\Filter\PropertyFilter; +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Serializer\Filter\PropertyFilter; use ApiPlatform\Metadata\ApiFilter; -use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter as BaseFilter; +use ApiPlatform\Doctrine\Orm\Filter as BaseFilter; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; @@ -543,19 +544,24 @@ class Translation extends AbstractDateTimed implements TranslationInterface protected Collection $folderTranslations; /** - * Language locale + * ISO 639-1 Language locale. * * fr or en for example * * @var string + * @see https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes * @Serializer\Groups({"translation", "document", "nodes_sources", "tag", "attribute", "folder", "log_sources"}) * @Serializer\Type("string") */ - #[ORM\Column(type: 'string', length: 10, unique: true)] + #[ORM\Column(type: 'string', length: 10, unique: true, nullable: false)] #[SymfonySerializer\Ignore] #[Assert\NotBlank] #[Assert\NotNull] #[Assert\Length(max: 10)] + #[ApiProperty( + description: 'Translation ISO 639-1 locale. See https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes', + example: 'fr', + )] private string $locale = ''; /** @@ -566,6 +572,10 @@ class Translation extends AbstractDateTimed implements TranslationInterface #[ORM\Column(name: 'override_locale', type: 'string', length: 10, unique: true, nullable: true)] #[SymfonySerializer\Ignore] #[Assert\Length(max: 10)] + #[ApiProperty( + description: 'Override standard locale with an other one (for example, `uk` instead of `en`)', + example: 'uk', + )] private ?string $overrideLocale = null; /** @@ -573,11 +583,15 @@ class Translation extends AbstractDateTimed implements TranslationInterface * @Serializer\Groups({"translation", "translation_base"}) * @Serializer\Type("string") */ - #[ORM\Column(type: 'string', unique: true)] + #[ORM\Column(type: 'string', length: 250, unique: true)] #[SymfonySerializer\Groups(['translation', 'translation_base'])] #[Assert\NotNull] #[Assert\NotBlank] #[Assert\Length(max: 250)] + #[ApiProperty( + description: 'Translation display name', + example: 'French', + )] private string $name = ''; /** @@ -587,6 +601,10 @@ class Translation extends AbstractDateTimed implements TranslationInterface */ #[ORM\Column(name: 'default_translation', type: 'boolean', nullable: false, options: ['default' => false])] #[SymfonySerializer\Groups(['translation', 'translation_base'])] + #[ApiProperty( + description: 'Is translation default one?', + example: 'true', + )] private bool $defaultTranslation = false; /** @@ -596,6 +614,10 @@ class Translation extends AbstractDateTimed implements TranslationInterface */ #[ORM\Column(type: 'boolean', nullable: false, options: ['default' => true])] #[SymfonySerializer\Groups(['translation', 'translation_base'])] + #[ApiProperty( + description: 'Is translation available publicly?', + example: 'true', + )] private bool $available = true; /** @@ -794,6 +816,10 @@ public function setOverrideLocale(?string $overrideLocale): Translation */ #[SymfonySerializer\SerializedName('locale')] #[SymfonySerializer\Groups(['translation_base'])] + #[ApiProperty( + description: 'Translation ISO 639-1 locale. See https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes', + example: 'fr', + )] public function getPreferredLocale(): string { return !empty($this->overrideLocale) ? $this->overrideLocale : $this->locale; diff --git a/src/Entity/UrlAlias.php b/src/Entity/UrlAlias.php index b937ab13..9f713fef 100644 --- a/src/Entity/UrlAlias.php +++ b/src/Entity/UrlAlias.php @@ -22,29 +22,20 @@ ] class UrlAlias extends AbstractEntity { - #[ORM\Column(type: 'string', unique: true)] + #[ORM\Column(type: 'string', length: 250, unique: true)] #[SymfonySerializer\Groups(['url_alias'])] #[Serializer\Groups(['url_alias'])] #[Assert\NotNull] #[Assert\NotBlank] + #[Assert\Length(max: 250)] #[RoadizAssert\UniqueNodeName] private string $alias = ''; #[ORM\ManyToOne(targetEntity: NodesSources::class, inversedBy: 'urlAliases')] - #[ORM\JoinColumn(name: 'ns_id', referencedColumnName: 'id')] + #[ORM\JoinColumn(name: 'ns_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')] #[SymfonySerializer\Ignore] #[Serializer\Exclude] - private ?NodesSources $nodeSource = null; - - /** - * Create a new UrlAlias linked to a NodeSource. - * - * @param NodesSources|null $nodeSource - */ - public function __construct(?NodesSources $nodeSource = null) - { - $this->setNodeSource($nodeSource); - } + private NodesSources $nodeSource; /** * @return string @@ -65,19 +56,12 @@ public function setAlias(string $alias): UrlAlias return $this; } - /** - * @return NodesSources|null - */ - public function getNodeSource(): ?NodesSources + public function getNodeSource(): NodesSources { return $this->nodeSource; } - /** - * @param NodesSources|null $nodeSource - * @return $this - */ - public function setNodeSource(?NodesSources $nodeSource): UrlAlias + public function setNodeSource(NodesSources $nodeSource): UrlAlias { $this->nodeSource = $nodeSource; return $this; diff --git a/src/Entity/User.php b/src/Entity/User.php index eeb57296..a5d39a62 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -10,16 +10,15 @@ use JMS\Serializer\Annotation as Serializer; use Rollerworks\Component\PasswordStrength\Validator\Constraints\PasswordStrength; use RZ\Roadiz\Core\AbstractEntities\AbstractHuman; +use RZ\Roadiz\CoreBundle\Form\Constraint\ValidFacebookName; use RZ\Roadiz\CoreBundle\Repository\UserRepository; use RZ\Roadiz\CoreBundle\Security\User\AdvancedUserInterface; -use RZ\Roadiz\Random\SaltGenerator; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Security\Core\User\EquatableInterface; use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Serializer\Annotation as SymfonySerializer; use Symfony\Component\Validator\Constraints as Assert; -use RZ\Roadiz\CoreBundle\Form\Constraint\ValidFacebookName; #[ ORM\Entity(repositoryClass: UserRepository::class), @@ -33,6 +32,8 @@ ORM\Index(columns: ["last_login"], name: "idx_users_last_login"), ORM\Index(columns: ["locked"], name: "idx_users_locked"), ORM\Index(columns: ["locale"], name: "idx_users_locale"), + ORM\Index(columns: ["created_at"], name: "idx_user_created_at"), + ORM\Index(columns: ["updated_at"], name: "idx_user_updated_at"), ORM\HasLifecycleCallbacks, UniqueEntity("email"), UniqueEntity("username") @@ -51,7 +52,7 @@ class User extends AbstractHuman implements UserInterface, AdvancedUserInterface * @Serializer\Groups({"user_personal", "human"}) * @var string|null */ - #[ORM\Column(type: 'string', unique: true, nullable: false)] + #[ORM\Column(type: 'string', length: 200, unique: true, nullable: false)] #[SymfonySerializer\Groups(['user_personal', 'human'])] #[Assert\NotNull] #[Assert\NotBlank] @@ -67,9 +68,10 @@ class User extends AbstractHuman implements UserInterface, AdvancedUserInterface #[SymfonySerializer\Ignore] protected bool $sendCreationConfirmationEmail = false; - #[ORM\Column(name: 'facebook_name', type: 'string', unique: false, nullable: true)] + #[ORM\Column(name: 'facebook_name', type: 'string', length: 128, unique: false, nullable: true)] #[SymfonySerializer\Groups(['user_social'])] #[Serializer\Groups(['user_social'])] + #[Assert\Length(max: 128)] #[ValidFacebookName] protected ?string $facebookName = null; @@ -94,8 +96,9 @@ class User extends AbstractHuman implements UserInterface, AdvancedUserInterface * @Serializer\Groups({"user_security"}) * @var string|null */ - #[ORM\Column(name: 'confirmation_token', type: 'string', unique: true, nullable: true)] + #[ORM\Column(name: 'confirmation_token', type: 'string', length: 128, unique: true, nullable: true)] #[SymfonySerializer\Groups(['user_security'])] + #[Assert\Length(max: 128)] protected ?string $confirmationToken = null; /** @@ -110,27 +113,20 @@ class User extends AbstractHuman implements UserInterface, AdvancedUserInterface * @Serializer\Groups({"user_personal", "log_user"}) * @var string */ - #[ORM\Column(type: 'string', unique: true)] + #[ORM\Column(type: 'string', length: 200, unique: true)] #[SymfonySerializer\Groups(['user_personal', 'log_user'])] #[Assert\NotNull] #[Assert\NotBlank] #[Assert\Length(max: 200)] private string $username = ''; - /** - * The salt to use for hashing. - */ - #[ORM\Column(name: 'salt', type: 'string')] - #[SymfonySerializer\Ignore] - #[Serializer\Exclude] - private string $salt = ''; - /** * Encrypted password. */ - #[ORM\Column(type: 'string', nullable: false)] + #[ORM\Column(type: 'string', length: 128, nullable: false)] #[SymfonySerializer\Ignore] #[Serializer\Exclude] + #[Assert\Length(max: 128)] private string $password = ''; /** @@ -224,6 +220,7 @@ class User extends AbstractHuman implements UserInterface, AdvancedUserInterface */ #[ORM\Column(name: 'locale', type: 'string', length: 7, nullable: true)] #[SymfonySerializer\Groups(['user'])] + #[Assert\Length(max: 7)] private ?string $locale = null; public function __construct() @@ -232,9 +229,6 @@ public function __construct() $this->groups = new ArrayCollection(); $this->sendCreationConfirmationEmail(false); $this->initAbstractDateTimed(); - - $saltGenerator = new SaltGenerator(); - $this->setSalt($saltGenerator->generateSalt()); } /** @@ -354,17 +348,7 @@ public function setPictureUrl(?string $pictureUrl): User */ public function getSalt(): ?string { - return $this->salt; - } - - /** - * @param string $salt - * @return $this - */ - public function setSalt(string $salt): User - { - $this->salt = $salt; - return $this; + return null; } /** @@ -874,8 +858,8 @@ public function __serialize(): array { return [ $this->password, - $this->salt, $this->username, + $this->getSalt(), $this->enabled, $this->id, $this->email, @@ -891,10 +875,11 @@ public function __serialize(): array public function __unserialize(array $data): void { + $salt = null; [ $this->password, - $this->salt, $this->username, + $salt, $this->enabled, $this->id, $this->email, @@ -961,10 +946,6 @@ public function isEqualTo(UserInterface $user): bool return false; } - if ($this->getSalt() !== $user->getSalt()) { - return false; - } - if ($this->getUsername() !== $user->getUsername()) { return false; } diff --git a/src/Entity/Webhook.php b/src/Entity/Webhook.php index 22f2caaa..840cff92 100644 --- a/src/Entity/Webhook.php +++ b/src/Entity/Webhook.php @@ -49,6 +49,7 @@ class Webhook extends AbstractDateTimed implements WebhookInterface */ #[ORM\Column(name: 'message_type', type: 'string', length: 255, nullable: true)] #[Serializer\Type('string')] + #[Assert\Length(max: 255)] protected ?string $messageType = null; /** diff --git a/src/EntityApi/AbstractApi.php b/src/EntityApi/AbstractApi.php index 6621841f..23d662b9 100644 --- a/src/EntityApi/AbstractApi.php +++ b/src/EntityApi/AbstractApi.php @@ -38,7 +38,6 @@ abstract public function getBy(array $criteria); * Return one entity matching criteria array. * * @param array $criteria - * * @return mixed */ abstract public function getOneBy(array $criteria); @@ -47,7 +46,6 @@ abstract public function getOneBy(array $criteria); * Count entities matching criteria array. * * @param array $criteria - * * @return int */ abstract public function countBy(array $criteria); diff --git a/src/EntityApi/NodeSourceApi.php b/src/EntityApi/NodeSourceApi.php index e3dc5a8f..d3821a8b 100644 --- a/src/EntityApi/NodeSourceApi.php +++ b/src/EntityApi/NodeSourceApi.php @@ -4,7 +4,7 @@ namespace RZ\Roadiz\CoreBundle\EntityApi; -use Doctrine\ORM\EntityRepository; +use Doctrine\ORM\NonUniqueResultException; use Doctrine\ORM\Tools\Pagination\Paginator; use RZ\Roadiz\CoreBundle\Entity\NodesSources; use RZ\Roadiz\CoreBundle\Entity\NodeType; @@ -15,16 +15,16 @@ class NodeSourceApi extends AbstractApi /** * @var class-string */ - protected string $repository = NodesSources::class; + protected string $nodeSourceClassName = NodesSources::class; /** * @param array|null $criteria - * @return string + * @return class-string */ - protected function getRepositoryName(array $criteria = null) + protected function getNodeSourceClassName(array $criteria = null): string { if (isset($criteria['node.nodeType']) && $criteria['node.nodeType'] instanceof NodeType) { - $this->repository = $criteria['node.nodeType']->getSourceEntityFullQualifiedClassName(); + $this->nodeSourceClassName = $criteria['node.nodeType']->getSourceEntityFullQualifiedClassName(); unset($criteria['node.nodeType']); } elseif ( isset($criteria['node.nodeType']) && @@ -32,21 +32,22 @@ protected function getRepositoryName(array $criteria = null) count($criteria['node.nodeType']) === 1 && $criteria['node.nodeType'][0] instanceof NodeType ) { - $this->repository = $criteria['node.nodeType'][0]->getSourceEntityFullQualifiedClassName(); + $this->nodeSourceClassName = $criteria['node.nodeType'][0]->getSourceEntityFullQualifiedClassName(); unset($criteria['node.nodeType']); } else { - $this->repository = NodesSources::class; + $this->nodeSourceClassName = NodesSources::class; } - return $this->repository; + return $this->nodeSourceClassName; } /** - * @return NodesSourcesRepository|EntityRepository + * @return NodesSourcesRepository */ - public function getRepository() + public function getRepository(): NodesSourcesRepository { - return $this->managerRegistry->getRepository($this->repository); + // @phpstan-ignore-next-line + return $this->managerRegistry->getRepository($this->nodeSourceClassName); } /** @@ -62,7 +63,7 @@ public function getBy( ?int $limit = null, ?int $offset = null ) { - $this->getRepositoryName($criteria); + $this->getNodeSourceClassName($criteria); return $this->getRepository() ->findBy( @@ -81,8 +82,7 @@ public function getBy( */ public function countBy(array $criteria) { - $this->getRepositoryName($criteria); - + $this->getNodeSourceClassName($criteria); return $this->getRepository() ->countBy( $criteria @@ -93,11 +93,11 @@ public function countBy(array $criteria) * @param array $criteria * @param array|null $order * @return null|NodesSources + * @throws NonUniqueResultException */ public function getOneBy(array $criteria, array $order = null) { - $this->getRepositoryName($criteria); - + $this->getNodeSourceClassName($criteria); return $this->getRepository() ->findOneBy( $criteria, @@ -123,19 +123,13 @@ public function searchBy( bool $onlyVisible = false, array $additionalCriteria = [] ) { - $repository = $this->getRepository(); - - if ($repository instanceof NodesSourcesRepository) { - return $this->getRepository() - ->findByTextQuery( - $textQuery, - $limit, - $nodeTypes, - $onlyVisible, - $additionalCriteria - ); - } - - return []; + return $this->getRepository() + ->findByTextQuery( + $textQuery, + $limit, + $nodeTypes, + $onlyVisible, + $additionalCriteria + ); } } diff --git a/src/EntityHandler/CustomFormFieldHandler.php b/src/EntityHandler/CustomFormFieldHandler.php index 368bb8fd..e7b8cbd5 100644 --- a/src/EntityHandler/CustomFormFieldHandler.php +++ b/src/EntityHandler/CustomFormFieldHandler.php @@ -11,38 +11,29 @@ /** * Handle operations with customForms fields entities. */ -class CustomFormFieldHandler extends AbstractHandler +final class CustomFormFieldHandler extends AbstractHandler { private ?CustomFormField $customFormField = null; - private CustomFormHandler $customFormHandler; - /** - * @return CustomFormField - */ - public function getCustomFormField(): ?CustomFormField - { - return $this->customFormField; - } /** * @param CustomFormField $customFormField * @return $this */ - public function setCustomFormField(CustomFormField $customFormField) + public function setCustomFormField(CustomFormField $customFormField): self { $this->customFormField = $customFormField; return $this; } /** - * Create a new custom-form-field handler with custom-form-field to handle. - * * @param ObjectManager $objectManager * @param CustomFormHandler $customFormHandler */ - public function __construct(ObjectManager $objectManager, CustomFormHandler $customFormHandler) - { + public function __construct( + ObjectManager $objectManager, + private readonly CustomFormHandler $customFormHandler + ) { parent::__construct($objectManager); - $this->customFormHandler = $customFormHandler; } /** @@ -57,11 +48,7 @@ public function cleanPositions(bool $setPositions = true): float throw new \BadMethodCallException('CustomForm is null'); } - if ($this->customFormField->getCustomForm() !== null) { - $this->customFormHandler->setCustomForm($this->customFormField->getCustomForm()); - return $this->customFormHandler->cleanFieldsPositions($setPositions); - } - - return 1; + $this->customFormHandler->setCustomForm($this->customFormField->getCustomForm()); + return $this->customFormHandler->cleanFieldsPositions($setPositions); } } diff --git a/src/EntityHandler/CustomFormHandler.php b/src/EntityHandler/CustomFormHandler.php index d77dd59e..721e121a 100644 --- a/src/EntityHandler/CustomFormHandler.php +++ b/src/EntityHandler/CustomFormHandler.php @@ -11,20 +11,11 @@ /** * Handle operations with node-type entities. */ -class CustomFormHandler extends AbstractHandler +final class CustomFormHandler extends AbstractHandler { protected ?CustomForm $customForm = null; - public function getCustomForm(): ?CustomForm - { - return $this->customForm; - } - - /** - * @param CustomForm $customForm - * @return $this - */ - public function setCustomForm(CustomForm $customForm) + public function setCustomForm(CustomForm $customForm): self { $this->customForm = $customForm; return $this; diff --git a/src/EntityHandler/DocumentHandler.php b/src/EntityHandler/DocumentHandler.php index 96b4bdbb..44e9d885 100644 --- a/src/EntityHandler/DocumentHandler.php +++ b/src/EntityHandler/DocumentHandler.php @@ -21,37 +21,39 @@ /** * Handle operations with documents entities. */ -class DocumentHandler extends AbstractHandler +final class DocumentHandler extends AbstractHandler { - protected ?DocumentInterface $document = null; - private FilesystemOperator $documentStorage; + private ?DocumentInterface $document = null; - public function __construct(ObjectManager $objectManager, FilesystemOperator $documentStorage) + public function __construct(ObjectManager $objectManager, private readonly FilesystemOperator $documentStorage) { parent::__construct($objectManager); - $this->documentStorage = $documentStorage; } /** * Get a Response object to force download document. * This method works for both private and public documents. * + * @param bool $asAttachment * @return StreamedResponse * @throws FilesystemException */ - public function getDownloadResponse(): StreamedResponse + public function getDownloadResponse(bool $asAttachment = true): StreamedResponse { if ($this->document->isLocal()) { $documentPath = $this->document->getMountPath(); if ($this->documentStorage->fileExists($documentPath)) { - return new StreamedResponse(function () use ($documentPath) { - \fpassthru($this->documentStorage->readStream($documentPath)); - }, Response::HTTP_OK, [ + $headers = [ "Content-Type" => $this->documentStorage->mimeType($documentPath), "Content-Length" => $this->documentStorage->fileSize($documentPath), - "Content-disposition" => "attachment; filename=\"" . basename($this->document->getFilename()) . "\"", - ]); + ]; + if ($asAttachment) { + $headers["Content-disposition"] = "attachment; filename=\"" . basename($this->document->getFilename()) . "\""; + } + return new StreamedResponse(function () use ($documentPath) { + \fpassthru($this->documentStorage->readStream($documentPath)); + }, Response::HTTP_OK, $headers); } } @@ -91,9 +93,9 @@ public function getDocument(): ?DocumentInterface /** * @param DocumentInterface $document - * @return DocumentHandler + * @return $this */ - public function setDocument(DocumentInterface $document): DocumentHandler + public function setDocument(DocumentInterface $document): self { $this->document = $document; return $this; diff --git a/src/EntityHandler/FolderHandler.php b/src/EntityHandler/FolderHandler.php index 2b01d6af..5cb16fdb 100644 --- a/src/EntityHandler/FolderHandler.php +++ b/src/EntityHandler/FolderHandler.php @@ -6,13 +6,12 @@ use Doctrine\Common\Collections\Criteria; use RZ\Roadiz\Core\Handlers\AbstractHandler; -use RZ\Roadiz\Core\AbstractEntities\LeafInterface; use RZ\Roadiz\CoreBundle\Entity\Folder; /** * Handle operations with folders entities. */ -class FolderHandler extends AbstractHandler +final class FolderHandler extends AbstractHandler { protected ?Folder $folder = null; @@ -28,7 +27,7 @@ public function getFolder(): Folder * @param Folder $folder * @return $this */ - public function setFolder(Folder $folder) + public function setFolder(Folder $folder): self { $this->folder = $folder; return $this; @@ -39,7 +38,7 @@ public function setFolder(Folder $folder) * * @return $this */ - private function removeChildren() + private function removeChildren(): self { /** @var Folder $folder */ foreach ($this->getFolder()->getChildren() as $folder) { @@ -57,7 +56,7 @@ private function removeChildren() * * @return $this */ - public function removeWithChildrenAndAssociations() + public function removeWithChildrenAndAssociations(): self { $this->removeChildren(); $this->objectManager->remove($this->getFolder()); @@ -69,49 +68,6 @@ public function removeWithChildrenAndAssociations() return $this; } - /** - * Return every folder’s parents. - * - * @deprecated Use directly Folder::getParents method. - * @return array - */ - public function getParents(): array - { - $parentsArray = []; - $parent = $this->getFolder(); - - do { - $parent = $parent->getParent(); - if ($parent !== null) { - $parentsArray[] = $parent; - } else { - break; - } - } while ($parent !== null); - - return array_reverse($parentsArray); - } - - /** - * Get folder full path using folder names. - * - * @deprecated Use directly Folder::getFullPath method. - * @return string - */ - public function getFullPath(): string - { - $parents = $this->getParents(); - $path = []; - - foreach ($parents as $parent) { - $path[] = $parent->getFolderName(); - } - - $path[] = $this->getFolder()->getFolderName(); - - return implode('/', $path); - } - /** * Clean position for current folder siblings. * diff --git a/src/EntityHandler/GroupHandler.php b/src/EntityHandler/GroupHandler.php index b4cfe577..9fb78ea9 100644 --- a/src/EntityHandler/GroupHandler.php +++ b/src/EntityHandler/GroupHandler.php @@ -11,7 +11,7 @@ /** * Handle operations with Group entities. */ -class GroupHandler extends AbstractHandler +final class GroupHandler extends AbstractHandler { private ?Group $group = null; diff --git a/src/EntityHandler/HandlerFactory.php b/src/EntityHandler/HandlerFactory.php index 8975ba83..860b941e 100644 --- a/src/EntityHandler/HandlerFactory.php +++ b/src/EntityHandler/HandlerFactory.php @@ -20,16 +20,10 @@ use RZ\Roadiz\CoreBundle\Entity\Tag; use RZ\Roadiz\CoreBundle\Entity\Translation; -class HandlerFactory implements HandlerFactoryInterface +final class HandlerFactory implements HandlerFactoryInterface { - private ContainerInterface $container; - - /** - * @param ContainerInterface $container - */ - public function __construct(ContainerInterface $container) + public function __construct(private readonly ContainerInterface $container) { - $this->container = $container; } /** diff --git a/src/EntityHandler/NodeHandler.php b/src/EntityHandler/NodeHandler.php index d9a9b408..3954b388 100644 --- a/src/EntityHandler/NodeHandler.php +++ b/src/EntityHandler/NodeHandler.php @@ -5,8 +5,10 @@ namespace RZ\Roadiz\CoreBundle\EntityHandler; use Doctrine\Common\Collections\Criteria; +use Doctrine\ORM\NonUniqueResultException; use Doctrine\Persistence\ObjectManager; -use RZ\Roadiz\Core\AbstractEntities\LeafInterface; +use RZ\Roadiz\Contracts\NodeType\NodeTypeFieldInterface; +use RZ\Roadiz\Core\Handlers\AbstractHandler; use RZ\Roadiz\CoreBundle\Entity\CustomForm; use RZ\Roadiz\CoreBundle\Entity\Node; use RZ\Roadiz\CoreBundle\Entity\NodesCustomForms; @@ -14,41 +16,28 @@ use RZ\Roadiz\CoreBundle\Entity\NodesToNodes; use RZ\Roadiz\CoreBundle\Entity\NodeTypeField; use RZ\Roadiz\CoreBundle\Entity\Translation; -use RZ\Roadiz\CoreBundle\Repository\NodeRepository; use RZ\Roadiz\CoreBundle\Node\NodeDuplicator; use RZ\Roadiz\CoreBundle\Node\NodeNamePolicyInterface; +use RZ\Roadiz\CoreBundle\Repository\NodeRepository; use RZ\Roadiz\CoreBundle\Security\Authorization\Chroot\NodeChrootResolver; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Workflow\Registry; -use Symfony\Component\Workflow\Workflow; -use RZ\Roadiz\Core\Handlers\AbstractHandler; +use Symfony\Component\Workflow\WorkflowInterface; /** * Handle operations with nodes entities. */ -class NodeHandler extends AbstractHandler +final class NodeHandler extends AbstractHandler { - protected NodeChrootResolver $chrootResolver; - private Registry $registry; private ?Node $node = null; - private NodeNamePolicyInterface $nodeNamePolicy; - /** - * @param ObjectManager $objectManager - * @param Registry $registry - * @param NodeChrootResolver $chrootResolver - * @param NodeNamePolicyInterface $nodeNamePolicy - */ final public function __construct( ObjectManager $objectManager, - Registry $registry, - NodeChrootResolver $chrootResolver, - NodeNamePolicyInterface $nodeNamePolicy + private readonly Registry $registry, + private readonly NodeChrootResolver $chrootResolver, + private readonly NodeNamePolicyInterface $nodeNamePolicy ) { parent::__construct($objectManager); - $this->registry = $registry; - $this->chrootResolver = $chrootResolver; - $this->nodeNamePolicy = $nodeNamePolicy; } protected function createSelf(): self @@ -74,9 +63,9 @@ public function getNode(): Node /** * @param Node $node - * @return NodeHandler + * @return $this */ - public function setNode(Node $node) + public function setNode(Node $node): self { $this->node = $node; return $this; @@ -89,11 +78,11 @@ public function setNode(Node $node) * @param bool $flush * @return $this */ - public function cleanCustomFormsFromField(NodeTypeField $field, bool $flush = true) + public function cleanCustomFormsFromField(NodeTypeFieldInterface $field, bool $flush = true): self { $nodesCustomForms = $this->objectManager ->getRepository(NodesCustomForms::class) - ->findBy(['node' => $this->getNode(), 'field' => $field]); + ->findBy(['node' => $this->getNode(), 'fieldName' => $field->getName()]); foreach ($nodesCustomForms as $ncf) { $this->objectManager->remove($ncf); @@ -117,16 +106,16 @@ public function cleanCustomFormsFromField(NodeTypeField $field, bool $flush = tr */ public function addCustomFormForField( CustomForm $customForm, - NodeTypeField $field, + NodeTypeFieldInterface $field, bool $flush = true, ?float $position = null - ) { + ): self { $ncf = new NodesCustomForms($this->getNode(), $customForm, $field); if (null === $position) { $latestPosition = $this->objectManager ->getRepository(NodesCustomForms::class) - ->getLatestPosition($this->getNode(), $field); + ->getLatestPositionForFieldName($this->getNode(), $field->getName()); $ncf->setPosition($latestPosition + 1); } else { $ncf->setPosition($position); @@ -151,9 +140,9 @@ public function getCustomFormsFromFieldName(string $fieldName): array { return $this->objectManager ->getRepository(CustomForm::class) - ->findByNodeAndField( + ->findByNodeAndFieldName( $this->getNode(), - $this->getNode()->getNodeType()->getFieldByName($fieldName) + $fieldName ); } @@ -164,7 +153,7 @@ public function getCustomFormsFromFieldName(string $fieldName): array * @param bool $flush * @return $this */ - public function cleanNodesFromField(NodeTypeField $field, bool $flush = true) + public function cleanNodesFromField(NodeTypeFieldInterface $field, bool $flush = true): self { $this->node->clearBNodesForField($field); @@ -184,7 +173,7 @@ public function cleanNodesFromField(NodeTypeField $field, bool $flush = true) * @param null|float $position * @return $this */ - public function addNodeForField(Node $node, NodeTypeField $field, bool $flush = true, ?float $position = null) + public function addNodeForField(Node $node, NodeTypeFieldInterface $field, bool $flush = true, ?float $position = null): self { $ntn = new NodesToNodes($this->getNode(), $node, $field); @@ -192,7 +181,7 @@ public function addNodeForField(Node $node, NodeTypeField $field, bool $flush = if (null === $position) { $latestPosition = $this->objectManager ->getRepository(NodesToNodes::class) - ->getLatestPosition($this->getNode(), $field); + ->getLatestPositionForFieldName($this->getNode(), $field->getName()); $ntn->setPosition($latestPosition + 1); } else { $ntn->setPosition($position); @@ -251,6 +240,7 @@ public function getReverseNodesFromFieldName(string $fieldName): array * @param Translation $translation * * @return null|NodesSources + * @deprecated Use Node::getNodeSourcesByTranslation() instead. */ public function getNodeSourceByTranslation($translation): ?NodesSources { @@ -264,7 +254,7 @@ public function getNodeSourceByTranslation($translation): ?NodesSources * * @return $this */ - private function removeChildren() + private function removeChildren(): self { /** @var Node $node */ foreach ($this->getNode()->getChildren() as $node) { @@ -280,7 +270,7 @@ private function removeChildren() * * @return $this */ - public function removeAssociations() + public function removeAssociations(): self { /** @var NodesSources $ns */ foreach ($this->getNode()->getNodeSources() as $ns) { @@ -297,7 +287,7 @@ public function removeAssociations() * * @return $this */ - public function removeWithChildrenAndAssociations() + public function removeWithChildrenAndAssociations(): self { $this->removeChildren(); $this->removeAssociations(); @@ -307,9 +297,9 @@ public function removeWithChildrenAndAssociations() } /** - * @return Workflow + * @return WorkflowInterface */ - private function getWorkflow(): Workflow + private function getWorkflow(): WorkflowInterface { return $this->registry->get($this->getNode()); } @@ -321,7 +311,7 @@ private function getWorkflow(): Workflow * * @return $this */ - public function softRemoveWithChildren() + public function softRemoveWithChildren(): self { $workflow = $this->getWorkflow(); if ($workflow->can($this->getNode(), 'delete')) { @@ -345,7 +335,7 @@ public function softRemoveWithChildren() * * @return $this */ - public function softUnremoveWithChildren() + public function softUnremoveWithChildren(): self { $workflow = $this->getWorkflow(); if ($workflow->can($this->getNode(), 'undelete')) { @@ -369,7 +359,7 @@ public function softUnremoveWithChildren() * * @return $this */ - public function publishWithChildren() + public function publishWithChildren(): self { $workflow = $this->getWorkflow(); if ($workflow->can($this->getNode(), 'publish')) { @@ -392,7 +382,7 @@ public function publishWithChildren() * * @return $this */ - public function archiveWithChildren() + public function archiveWithChildren(): self { $workflow = $this->getWorkflow(); if ($workflow->can($this->getNode(), 'archive')) { @@ -409,17 +399,6 @@ public function archiveWithChildren() return $this; } - /** - * Return if is in Newsletter Node. - * - * @deprecated Just here not to break themes. - * @return bool - */ - public function isRelatedToNewsletter(): bool - { - return false; - } - /** * Return if part of Node offspring. * @@ -550,6 +529,7 @@ public function cleanRootNodesPositions(bool $setPositions = true): float * Return all node offspring id. * * @return array + * @deprecated Use NodeRepository::findAllOffspringIdByNode() instead. */ public function getAllOffspringId(): array { @@ -561,7 +541,7 @@ public function getAllOffspringId(): array * * @return $this */ - public function makeHome() + public function makeHome(): self { $defaults = $this->getRepository() ->setDisplayingNotPublishedNodes(true) @@ -583,7 +563,7 @@ public function makeHome() * @return Node * @deprecated Use NodeDuplicator::duplicate() instead. */ - public function duplicate() + public function duplicate(): Node { $duplicator = new NodeDuplicator( $this->getNode(), @@ -596,15 +576,17 @@ public function duplicate() /** * Get previous node from hierarchy. * - * @param array|null $criteria - * @param array|null $order + * @param array|null $criteria + * @param array|null $order * * @return Node|null + * @throws NonUniqueResultException + * @deprecated Use NodeRepository::findPreviousNode() instead. */ public function getPrevious( ?array $criteria = null, ?array $order = null - ) { + ): ?Node { if ($this->getNode()->getPosition() <= 1) { return null; } @@ -637,15 +619,17 @@ public function getPrevious( /** * Get next node from hierarchy. * - * @param array|null $criteria - * @param array|null $order + * @param array|null $criteria + * @param array|null $order * * @return Node|null + * @throws NonUniqueResultException + * @deprecated Use NodeRepository::findNextNode() instead. */ public function getNext( ?array $criteria = null, ?array $order = null - ) { + ): ?Node { if (null === $criteria) { $criteria = []; } @@ -675,7 +659,7 @@ public function getNext( /** * @return NodeRepository */ - public function getRepository(): NodeRepository + protected function getRepository(): NodeRepository { return $this->objectManager->getRepository(Node::class); } diff --git a/src/EntityHandler/NodeTypeFieldHandler.php b/src/EntityHandler/NodeTypeFieldHandler.php index a1542bb8..2a17f9d5 100644 --- a/src/EntityHandler/NodeTypeFieldHandler.php +++ b/src/EntityHandler/NodeTypeFieldHandler.php @@ -12,9 +12,8 @@ /** * Handle operations with node-type fields entities. */ -class NodeTypeFieldHandler extends AbstractHandler +final class NodeTypeFieldHandler extends AbstractHandler { - private HandlerFactory $handlerFactory; private ?NodeTypeField $nodeTypeField = null; public function getNodeTypeField(): NodeTypeField @@ -35,16 +34,9 @@ public function setNodeTypeField(NodeTypeField $nodeTypeField): self return $this; } - /** - * Create a new node-type-field handler with node-type-field to handle. - * - * @param ObjectManager $objectManager - * @param HandlerFactory $handlerFactory - */ - public function __construct(ObjectManager $objectManager, HandlerFactory $handlerFactory) + public function __construct(ObjectManager $objectManager, private readonly HandlerFactory $handlerFactory) { parent::__construct($objectManager); - $this->handlerFactory = $handlerFactory; } /** diff --git a/src/EntityHandler/NodeTypeHandler.php b/src/EntityHandler/NodeTypeHandler.php index 8c6c7a39..e391a276 100644 --- a/src/EntityHandler/NodeTypeHandler.php +++ b/src/EntityHandler/NodeTypeHandler.php @@ -8,8 +8,8 @@ use Doctrine\Persistence\ObjectManager; use JMS\Serializer\SerializationContext; use JMS\Serializer\SerializerInterface; +use Psr\Log\LoggerInterface; use RZ\Roadiz\Core\Handlers\AbstractHandler; -use RZ\Roadiz\CoreBundle\Doctrine\SchemaUpdater; use RZ\Roadiz\CoreBundle\Entity\Node; use RZ\Roadiz\CoreBundle\Entity\NodeType; use RZ\Roadiz\CoreBundle\Entity\NodeTypeField; @@ -24,17 +24,9 @@ /** * Handle operations with node-type entities. */ -class NodeTypeHandler extends AbstractHandler +final class NodeTypeHandler extends AbstractHandler { private ?NodeType $nodeType = null; - private EntityGeneratorFactory $entityGeneratorFactory; - private ApiResourceGenerator $apiResourceGenerator; - private HandlerFactory $handlerFactory; - private string $generatedEntitiesDir; - private SerializerInterface $serializer; - private string $serializedNodeTypesDir; - private string $importFilesConfigPath; - private string $kernelProjectDir; /** * @return NodeType @@ -51,7 +43,7 @@ public function getNodeType(): NodeType * @param NodeType $nodeType * @return $this */ - public function setNodeType(NodeType $nodeType) + public function setNodeType(NodeType $nodeType): self { $this->nodeType = $nodeType; return $this; @@ -59,24 +51,17 @@ public function setNodeType(NodeType $nodeType) public function __construct( ObjectManager $objectManager, - EntityGeneratorFactory $entityGeneratorFactory, - HandlerFactory $handlerFactory, - SerializerInterface $serializer, - ApiResourceGenerator $apiResourceGenerator, - string $generatedEntitiesDir, - string $serializedNodeTypesDir, - string $importFilesConfigPath, - string $kernelProjectDir + private readonly EntityGeneratorFactory $entityGeneratorFactory, + private readonly HandlerFactory $handlerFactory, + private readonly SerializerInterface $serializer, + private readonly ApiResourceGenerator $apiResourceGenerator, + private readonly LoggerInterface $logger, + private readonly string $generatedEntitiesDir, + private readonly string $serializedNodeTypesDir, + private readonly string $importFilesConfigPath, + private readonly string $kernelProjectDir ) { parent::__construct($objectManager); - $this->entityGeneratorFactory = $entityGeneratorFactory; - $this->handlerFactory = $handlerFactory; - $this->generatedEntitiesDir = $generatedEntitiesDir; - $this->serializer = $serializer; - $this->serializedNodeTypesDir = $serializedNodeTypesDir; - $this->importFilesConfigPath = $importFilesConfigPath; - $this->kernelProjectDir = $kernelProjectDir; - $this->apiResourceGenerator = $apiResourceGenerator; } public function getGeneratedEntitiesFolder(): string @@ -106,6 +91,11 @@ public function removeSourceEntityClass(): bool if ($fileSystem->exists($repositoryFile) && is_file($repositoryFile)) { $fileSystem->remove($repositoryFile); } + $this->logger->info('Entity class file and repository have been removed.', [ + 'nodeType' => $this->nodeType->getName(), + 'file' => $file, + 'repositoryFile' => $repositoryFile, + ]); return true; } @@ -254,6 +244,11 @@ public function generateSourceEntityClass(): bool \clearstatcache(true, $file); \clearstatcache(true, $repositoryFile); + $this->logger->info('Entity class file and repository have been generated.', [ + 'nodeType' => $this->nodeType->getName(), + 'file' => $file, + 'repositoryFile' => $repositoryFile, + ]); return true; } diff --git a/src/EntityHandler/NodesSourcesHandler.php b/src/EntityHandler/NodesSourcesHandler.php index 860fa692..55b2eb90 100644 --- a/src/EntityHandler/NodesSourcesHandler.php +++ b/src/EntityHandler/NodesSourcesHandler.php @@ -6,6 +6,8 @@ use Doctrine\ORM\EntityRepository; use Doctrine\Persistence\ObjectManager; +use RZ\Roadiz\Contracts\NodeType\NodeTypeFieldInterface; +use RZ\Roadiz\Core\Handlers\AbstractHandler; use RZ\Roadiz\CoreBundle\Bag\Settings; use RZ\Roadiz\CoreBundle\Entity\Document; use RZ\Roadiz\CoreBundle\Entity\Node; @@ -13,32 +15,21 @@ use RZ\Roadiz\CoreBundle\Entity\NodesSourcesDocuments; use RZ\Roadiz\CoreBundle\Entity\NodeTypeField; use RZ\Roadiz\CoreBundle\Entity\Tag; -use RZ\Roadiz\CoreBundle\Repository\NodesSourcesRepository; -use RZ\Roadiz\Core\Handlers\AbstractHandler; /** * Handle operations with node-sources entities. */ -class NodesSourcesHandler extends AbstractHandler +final class NodesSourcesHandler extends AbstractHandler { - protected ?NodesSources $nodeSource = null; + private ?NodesSources $nodeSource = null; /** * @var array|null */ - protected ?array $parentsNodeSources = null; - protected Settings $settingsBag; + private ?array $parentsNodeSources = null; - /** - * Create a new node-source handler with node-source to handle. - * - * @param ObjectManager $objectManager - * @param Settings $settingsBag - */ - public function __construct(ObjectManager $objectManager, Settings $settingsBag) + public function __construct(ObjectManager $objectManager, private readonly Settings $settingsBag) { parent::__construct($objectManager); - - $this->settingsBag = $settingsBag; } /** @@ -62,9 +53,9 @@ public function getNodeSource(): NodesSources /** * @param NodesSources $nodeSource - * @return NodesSourcesHandler + * @return $this */ - public function setNodeSource(NodesSources $nodeSource) + public function setNodeSource(NodesSources $nodeSource): self { $this->nodeSource = $nodeSource; return $this; @@ -77,7 +68,7 @@ public function setNodeSource(NodesSources $nodeSource) * @param bool $flush * @return $this */ - public function cleanDocumentsFromField(NodeTypeField $field, bool $flush = true) + public function cleanDocumentsFromField(NodeTypeFieldInterface $field, bool $flush = true): self { $this->nodeSource->clearDocumentsByFields($field); @@ -99,17 +90,17 @@ public function cleanDocumentsFromField(NodeTypeField $field, bool $flush = true */ public function addDocumentForField( Document $document, - NodeTypeField $field, + NodeTypeFieldInterface $field, bool $flush = true, ?float $position = null - ) { + ): self { $nsDoc = new NodesSourcesDocuments($this->nodeSource, $document, $field); if (!$this->nodeSource->hasNodesSourcesDocuments($nsDoc)) { if (null === $position) { $latestPosition = $this->objectManager ->getRepository(NodesSourcesDocuments::class) - ->getLatestPosition($this->nodeSource, $field); + ->getLatestPositionForFieldName($this->nodeSource, $field->getName()); $nsDoc->setPosition($latestPosition + 1); } else { @@ -130,19 +121,16 @@ public function addDocumentForField( * * @param string $fieldName Name of the node-type field * @return array + * @deprecated Use directly NodesSources::getDocumentsByFieldsWithName */ public function getDocumentsFromFieldName(string $fieldName): array { - $field = $this->nodeSource->getNode()->getNodeType()->getFieldByName($fieldName); - if (null !== $field) { - return $this->objectManager - ->getRepository(Document::class) - ->findByNodeSourceAndField( - $this->nodeSource, - $field - ); - } - return []; + return $this->objectManager + ->getRepository(Document::class) + ->findByNodeSourceAndFieldName( + $this->nodeSource, + $fieldName + ); } /** @@ -178,7 +166,7 @@ public function getParent(): ?NodesSources * * @param array|null $criteria * @return array - * @throws \Doctrine\ORM\NonUniqueResultException + * @deprecated Use NodesSourcesRepository::findParents */ public function getParents( array $criteria = null @@ -223,6 +211,7 @@ public function getParents( * @param array|null $order Non default ordering * * @return array + * @deprecated Use TreeWalker or NodesSourcesRepository::findChildren */ public function getChildren( array $criteria = null, @@ -258,6 +247,7 @@ public function getChildren( * @param array|null $order * * @return NodesSources|null + * @deprecated Use NodesSourcesRepository::findFirstChild */ public function getFirstChild( array $criteria = null, @@ -292,6 +282,7 @@ public function getFirstChild( * @param array|null $order * * @return NodesSources|null + * @deprecated Use NodesSourcesRepository::findLastChild */ public function getLastChild( array $criteria = null, @@ -327,6 +318,7 @@ public function getLastChild( * @param array|null $order * * @return NodesSources|null + * @deprecated Use NodesSourcesRepository::findFirstSibling */ public function getFirstSibling( array $criteria = null, @@ -349,6 +341,7 @@ public function getFirstSibling( * @param array|null $order * * @return NodesSources|null + * @deprecated Use NodesSourcesRepository::findLastSibling */ public function getLastSibling( array $criteria = null, @@ -371,6 +364,7 @@ public function getLastSibling( * @param array|null $order * * @return NodesSources|null + * @deprecated Use NodesSourcesRepository::findPrevious */ public function getPrevious( array $criteria = null, @@ -417,6 +411,7 @@ public function getPrevious( * @param array|null $order * * @return NodesSources|null + * @deprecated Use NodesSourcesRepository::findNext */ public function getNext( array $criteria = null, @@ -455,9 +450,10 @@ public function getNext( /** * Get node tags with current source translation. * - * @return array + * @return iterable + * @deprecated Use TagRepository::findByNodesSources */ - public function getTags() + public function getTags(): iterable { /** * @phpstan-ignore-next-line @@ -490,7 +486,6 @@ public function getSEO(): array 'description' => ($this->nodeSource->getMetaDescription() != "") ? $this->nodeSource->getMetaDescription() : $this->nodeSource->getTitle() . ', ' . $this->settingsBag->get('seo_description'), - 'keywords' => $this->nodeSource->getMetaKeywords(), ]; } @@ -501,7 +496,7 @@ public function getSEO(): array * * @return array Collection of nodes */ - public function getNodesFromFieldName(string $fieldName) + public function getNodesFromFieldName(string $fieldName): array { $field = $this->nodeSource->getNode()->getNodeType()->getFieldByName($fieldName); if (null !== $field) { @@ -523,7 +518,7 @@ public function getNodesFromFieldName(string $fieldName) * * @return array Collection of nodes */ - public function getReverseNodesFromFieldName(string $fieldName) + public function getReverseNodesFromFieldName(string $fieldName): array { $field = $this->nodeSource->getNode()->getNodeType()->getFieldByName($fieldName); if (null !== $field) { diff --git a/src/EntityHandler/TagHandler.php b/src/EntityHandler/TagHandler.php index b99e28c2..b995f460 100644 --- a/src/EntityHandler/TagHandler.php +++ b/src/EntityHandler/TagHandler.php @@ -13,26 +13,15 @@ /** * Handle operations with tags entities. */ -class TagHandler extends AbstractHandler +final class TagHandler extends AbstractHandler { private ?Tag $tag = null; - /** - * @return Tag - */ - public function getTag(): Tag - { - if (null === $this->tag) { - throw new \BadMethodCallException('Tag is null'); - } - return $this->tag; - } - /** * @param Tag $tag * @return $this */ - public function setTag(Tag $tag) + public function setTag(Tag $tag): self { $this->tag = $tag; return $this; @@ -43,7 +32,7 @@ public function setTag(Tag $tag) * * @return $this */ - private function removeChildren() + private function removeChildren(): self { /** @var Tag $tag */ foreach ($this->tag->getChildren() as $tag) { @@ -59,7 +48,7 @@ private function removeChildren() * * @return $this */ - public function removeAssociations() + public function removeAssociations(): self { foreach ($this->tag->getTranslatedTags() as $tt) { $this->objectManager->remove($tt); @@ -73,7 +62,7 @@ public function removeAssociations() * * @return $this */ - public function removeWithChildrenAndAssociations() + public function removeWithChildrenAndAssociations(): self { $this->removeChildren(); $this->removeAssociations(); @@ -88,120 +77,6 @@ public function removeWithChildrenAndAssociations() return $this; } - /** - * @return array Array of Translation - * @deprecated Do not query DB here - */ - public function getAvailableTranslations() - { - $query = $this->objectManager - ->createQuery(' - SELECT tr - FROM RZ\Roadiz\CoreBundle\Entity\Translation tr - INNER JOIN tr.tagTranslations tt - INNER JOIN tt.tag t - WHERE t.id = :tag_id') - ->setParameter('tag_id', $this->tag->getId()); - - try { - return $query->getResult(); - } catch (NoResultException $e) { - return []; - } - } - /** - * @return array Array of Translation id - * @deprecated Do not query DB here - */ - public function getAvailableTranslationsId() - { - $query = $this->objectManager - ->createQuery(' - SELECT tr.id FROM RZ\Roadiz\CoreBundle\Entity\Tag t - INNER JOIN t.translatedTags tt - INNER JOIN tt.translation tr - WHERE t.id = :tag_id') - ->setParameter('tag_id', $this->tag->getId()); - - try { - $simpleArray = []; - $complexArray = $query->getScalarResult(); - foreach ($complexArray as $subArray) { - $simpleArray[] = $subArray['id']; - } - - return $simpleArray; - } catch (NoResultException $e) { - return []; - } - } - - /** - * @return array Array of Translation - * @deprecated Do not query DB here - */ - public function getUnavailableTranslations() - { - $query = $this->objectManager - ->createQuery(' - SELECT tr FROM RZ\Roadiz\CoreBundle\Entity\Translation tr - WHERE tr.id NOT IN (:translations_id)') - ->setParameter('translations_id', $this->getAvailableTranslationsId()); - - try { - return $query->getResult(); - } catch (NoResultException $e) { - return []; - } - } - - /** - * @return array Array of Translation id - * @deprecated Do not query DB here - */ - public function getUnavailableTranslationsId() - { - /** @var Query $query */ - $query = $this->objectManager - ->createQuery(' - SELECT t.id FROM RZ\Roadiz\CoreBundle\Entity\Translation t - WHERE t.id NOT IN (:translations_id)') - ->setParameter('translations_id', $this->getAvailableTranslationsId()); - - try { - $simpleArray = []; - $complexArray = $query->getScalarResult(); - foreach ($complexArray as $subArray) { - $simpleArray[] = $subArray['id']; - } - - return $simpleArray; - } catch (NoResultException $e) { - return []; - } - } - - /** - * Return every tag’s parents. - * @deprecated Use directly Tag::getParents - * @return array - */ - public function getParents() - { - return $this->tag->getParents(); - } - - /** - * Get tag full path using tag names. - * - * @deprecated Use directly Tag::getFullPath - * @return string - */ - public function getFullPath(): string - { - return $this->tag->getFullPath(); - } - /** * Clean position for current tag siblings. * diff --git a/src/EntityHandler/TranslationHandler.php b/src/EntityHandler/TranslationHandler.php index 7a9d9673..6f3f30ea 100644 --- a/src/EntityHandler/TranslationHandler.php +++ b/src/EntityHandler/TranslationHandler.php @@ -14,27 +14,16 @@ /** * Handle operations with translations entities. */ -class TranslationHandler extends AbstractHandler +final class TranslationHandler extends AbstractHandler { private ?TranslationInterface $translation = null; - /** - * @return TranslationInterface - */ - public function getTranslation(): TranslationInterface - { - if (null === $this->translation) { - throw new \BadMethodCallException('Translation is null'); - } - return $this->translation; - } - /** * @param TranslationInterface $translation * * @return $this */ - public function setTranslation(TranslationInterface $translation) + public function setTranslation(TranslationInterface $translation): self { $this->translation = $translation; return $this; @@ -45,7 +34,7 @@ public function setTranslation(TranslationInterface $translation) * * @return $this */ - public function makeDefault() + public function makeDefault(): self { $defaults = $this->objectManager ->getRepository(Translation::class) diff --git a/src/Event/NodesSources/NodesSourcesPathGeneratingEvent.php b/src/Event/NodesSources/NodesSourcesPathGeneratingEvent.php index b2cd984b..2efc28d3 100644 --- a/src/Event/NodesSources/NodesSourcesPathGeneratingEvent.php +++ b/src/Event/NodesSources/NodesSourcesPathGeneratingEvent.php @@ -11,46 +11,14 @@ final class NodesSourcesPathGeneratingEvent extends Event { - /** - * @var bool - */ - protected $forceLocaleWithUrlAlias; - /** - * @var Theme|null - */ - private $theme; - /** - * @var NodesSources|null - */ - private $nodeSource; - /** - * @var array|null - */ - private $parameters; - /** - * @var RequestContext|null - */ - private $requestContext; - /** - * @var bool - */ - private $forceLocale = false; - /** - * @var string|null - */ - private $path; + private ?string $path; /** * @var bool Tells Node Router to prepend request context information to path or not. */ - private $isComplete = false; - /** - * @var bool - */ - protected $containsScheme = false; + private bool $isComplete = false; + protected bool $containsScheme = false; /** - * NodesSourcesPathGeneratingEvent constructor. - * * @param Theme|null $theme * @param NodesSources|null $nodeSource * @param RequestContext|null $requestContext @@ -59,19 +27,13 @@ final class NodesSourcesPathGeneratingEvent extends Event * @param bool $forceLocaleWithUrlAlias */ public function __construct( - ?Theme $theme, - ?NodesSources $nodeSource, - ?RequestContext $requestContext, - array $parameters = [], - bool $forceLocale = false, - bool $forceLocaleWithUrlAlias = false + private readonly ?Theme $theme, + private ?NodesSources $nodeSource, + private readonly ?RequestContext $requestContext, + private array $parameters = [], + private readonly bool $forceLocale = false, + private bool $forceLocaleWithUrlAlias = false ) { - $this->theme = $theme; - $this->nodeSource = $nodeSource; - $this->requestContext = $requestContext; - $this->forceLocale = $forceLocale; - $this->parameters = $parameters; - $this->forceLocaleWithUrlAlias = $forceLocaleWithUrlAlias; } /** diff --git a/src/Event/Redirection/PostCreatedRedirectionEvent.php b/src/Event/Redirection/PostCreatedRedirectionEvent.php new file mode 100644 index 00000000..13014c4f --- /dev/null +++ b/src/Event/Redirection/PostCreatedRedirectionEvent.php @@ -0,0 +1,9 @@ +redirection = $redirection; + } + + /** + * @return Redirection|null + */ + public function getRedirection(): ?Redirection + { + return $this->redirection; + } + + /** + * @param Redirection|null $redirection + * @return RedirectionEvent + */ + public function setRedirection(?Redirection $redirection): RedirectionEvent + { + $this->redirection = $redirection; + return $this; + } +} diff --git a/src/EventSubscriber/AssetsCacheEventSubscriber.php b/src/EventSubscriber/AssetsCacheEventSubscriber.php index 1e729545..664344d0 100644 --- a/src/EventSubscriber/AssetsCacheEventSubscriber.php +++ b/src/EventSubscriber/AssetsCacheEventSubscriber.php @@ -11,23 +11,16 @@ final class AssetsCacheEventSubscriber implements EventSubscriberInterface { - private AssetsFileClearer $assetsClearer; - private LoggerInterface $logger; - - public function __construct(AssetsFileClearer $assetsClearer, LoggerInterface $logger) - { - $this->assetsClearer = $assetsClearer; - $this->logger = $logger; + public function __construct( + private readonly AssetsFileClearer $assetsClearer, + private readonly LoggerInterface $logger + ) { } - /** - * @inheritDoc - */ public static function getSubscribedEvents(): array { return [ - CachePurgeAssetsRequestEvent::class => ['onPurgeAssetsRequest', 0], - '\RZ\Roadiz\Core\Events\Cache\CachePurgeAssetsRequestEvent' => ['onPurgeAssetsRequest', 0], + CachePurgeAssetsRequestEvent::class => ['onPurgeAssetsRequest', 0] ]; } diff --git a/src/EventSubscriber/AttributeValueIndexingSubscriber.php b/src/EventSubscriber/AttributeValueIndexingSubscriber.php deleted file mode 100644 index bd4a3e6b..00000000 --- a/src/EventSubscriber/AttributeValueIndexingSubscriber.php +++ /dev/null @@ -1,90 +0,0 @@ - 'onNodeSourceIndexing', - ]; - } - - public function onNodeSourceIndexing(NodesSourcesIndexingEvent $event): void - { - if ($event->getNodeSource()->getNode()->getAttributeValues()->count() === 0) { - return; - } - - $associations = $event->getAssociations(); - $attributeValues = $event->getNodeSource() - ->getNode() - ->getAttributesValuesForTranslation($event->getNodeSource()->getTranslation()); - - /** @var AttributeValueInterface $attributeValue */ - foreach ($attributeValues as $attributeValue) { - if ($attributeValue->getAttribute()->isSearchable()) { - $data = $attributeValue->getAttributeValueTranslation( - $event->getNodeSource()->getTranslation() - )->getValue(); - if (null === $data) { - $data = $attributeValue->getAttributeValueTranslations()->first()->getValue(); - } - if (null !== $data) { - switch ($attributeValue->getType()) { - case AttributeInterface::DATETIME_T: - case AttributeInterface::DATE_T: - if ($data instanceof \DateTime) { - $fieldName = $attributeValue->getAttribute()->getCode() . '_dt'; - $associations[$fieldName] = $data->format('Y-m-d\TH:i:s'); - } - break; - case AttributeInterface::STRING_T: - $fieldName = $attributeValue->getAttribute()->getCode(); - /* - * Use locale to create field name - * with right language - */ - if ( - in_array( - $event->getNodeSource()->getTranslation()->getLocale(), - AbstractSolarium::$availableLocalizedTextFields - ) - ) { - $lang = $event->getNodeSource()->getTranslation()->getLocale(); - $fieldName .= '_txt_' . $lang; - } else { - $lang = null; - $fieldName .= '_t'; - } - /* - * Strip Markdown syntax - */ - $content = $event->getSolariumDocument()->cleanTextContent($data); - $associations[$fieldName] = $content; - $associations['collection_txt'][] = $content; - if (null !== $lang) { - // Compile all text content into a single localized text field. - $associations['collection_txt_' . $lang] = implode(PHP_EOL, $associations['collection_txt']); - } - break; - } - } - } - } - - $event->setAssociations($associations); - } -} diff --git a/src/EventSubscriber/AutomaticWebhookSubscriber.php b/src/EventSubscriber/AutomaticWebhookSubscriber.php index 1e6ba43f..fb29b2fc 100644 --- a/src/EventSubscriber/AutomaticWebhookSubscriber.php +++ b/src/EventSubscriber/AutomaticWebhookSubscriber.php @@ -25,45 +25,25 @@ final class AutomaticWebhookSubscriber implements EventSubscriberInterface { - private WebhookDispatcher $webhookDispatcher; - private HandlerFactoryInterface $handlerFactory; - private ManagerRegistry $managerRegistry; - - /** - * @param WebhookDispatcher $webhookDispatcher - * @param ManagerRegistry $managerRegistry - * @param HandlerFactoryInterface $handlerFactory - */ public function __construct( - WebhookDispatcher $webhookDispatcher, - ManagerRegistry $managerRegistry, - HandlerFactoryInterface $handlerFactory + private readonly WebhookDispatcher $webhookDispatcher, + private readonly ManagerRegistry $managerRegistry, + private readonly HandlerFactoryInterface $handlerFactory ) { - $this->webhookDispatcher = $webhookDispatcher; - $this->handlerFactory = $handlerFactory; - $this->managerRegistry = $managerRegistry; } public static function getSubscribedEvents(): array { return [ - 'workflow.node.completed' => ['onAutomaticWebhook'], + 'workflow.node.completed' => 'onAutomaticWebhook', NodeVisibilityChangedEvent::class => 'onAutomaticWebhook', - '\RZ\Roadiz\Core\Events\Node\NodeVisibilityChangedEvent' => 'onAutomaticWebhook', NodesSourcesPreUpdatedEvent::class => 'onAutomaticWebhook', - '\RZ\Roadiz\Core\Events\NodesSources\NodesSourcesPreUpdatedEvent' => 'onAutomaticWebhook', NodesSourcesDeletedEvent::class => 'onAutomaticWebhook', - '\RZ\Roadiz\Core\Events\NodesSources\NodesSourcesDeletedEvent' => 'onAutomaticWebhook', NodeUpdatedEvent::class => 'onAutomaticWebhook', - '\RZ\Roadiz\Core\Events\Node\NodeUpdatedEvent' => 'onAutomaticWebhook', NodeDeletedEvent::class => 'onAutomaticWebhook', - '\RZ\Roadiz\Core\Events\Node\NodeDeletedEvent' => 'onAutomaticWebhook', NodeTaggedEvent::class => 'onAutomaticWebhook', - '\RZ\Roadiz\Core\Events\Node\NodeTaggedEvent' => 'onAutomaticWebhook', TagUpdatedEvent::class => 'onAutomaticWebhook', - '\RZ\Roadiz\Core\Events\Tag\TagUpdatedEvent' => 'onAutomaticWebhook', DocumentTranslationUpdatedEvent::class => 'onAutomaticWebhook', - '\RZ\Roadiz\Core\Events\DocumentTranslationUpdatedEvent' => 'onAutomaticWebhook', DocumentUpdatedEvent::class => 'onAutomaticWebhook', ]; } @@ -115,8 +95,6 @@ private function isEventSubjectInRootNode(mixed $event, ?Node $rootNode): bool */ return true; } - /** @var Node|null $subject */ - $subject = null; switch (true) { case $event instanceof Event: diff --git a/src/EventSubscriber/CloudflareCacheEventSubscriber.php b/src/EventSubscriber/CloudflareCacheEventSubscriber.php index 04b513bd..48fd5f2e 100644 --- a/src/EventSubscriber/CloudflareCacheEventSubscriber.php +++ b/src/EventSubscriber/CloudflareCacheEventSubscriber.php @@ -24,27 +24,12 @@ final class CloudflareCacheEventSubscriber implements EventSubscriberInterface { - private LoggerInterface $logger; - private MessageBusInterface $bus; - private UrlGeneratorInterface $urlGenerator; - private ReverseProxyCacheLocator $reverseProxyCacheLocator; - - /** - * @param MessageBusInterface $bus - * @param ReverseProxyCacheLocator $reverseProxyCacheLocator - * @param UrlGeneratorInterface $urlGenerator - * @param LoggerInterface $logger - */ public function __construct( - MessageBusInterface $bus, - ReverseProxyCacheLocator $reverseProxyCacheLocator, - UrlGeneratorInterface $urlGenerator, - LoggerInterface $logger + private readonly MessageBusInterface $bus, + private readonly ReverseProxyCacheLocator $reverseProxyCacheLocator, + private readonly UrlGeneratorInterface $urlGenerator, + private readonly LoggerInterface $logger ) { - $this->logger = $logger; - $this->bus = $bus; - $this->reverseProxyCacheLocator = $reverseProxyCacheLocator; - $this->urlGenerator = $urlGenerator; } /** * @inheritDoc @@ -53,25 +38,15 @@ public static function getSubscribedEvents(): array { return [ CachePurgeRequestEvent::class => ['onBanRequest', 3], - '\RZ\Roadiz\Core\Events\Cache\CachePurgeRequestEvent' => ['onBanRequest', 3], NodesSourcesUpdatedEvent::class => ['onPurgeRequest', 3], - '\RZ\Roadiz\Core\Events\NodesSources\NodesSourcesUpdatedEvent' => ['onPurgeRequest', 3], ]; } - /** - * @return bool - */ protected function supportConfig(): bool { return null !== $this->reverseProxyCacheLocator->getCloudflareProxyCache(); } - /** - * @param CachePurgeRequestEvent $event - * @throws \GuzzleHttp\Exception\GuzzleException - * @return void - */ public function onBanRequest(CachePurgeRequestEvent $event): void { if (!$this->supportConfig()) { @@ -109,11 +84,6 @@ public function onBanRequest(CachePurgeRequestEvent $event): void } } - /** - * @param NodesSourcesUpdatedEvent $event - * - * @throws \GuzzleHttp\Exception\GuzzleException - */ public function onPurgeRequest(NodesSourcesUpdatedEvent $event): void { if (!$this->supportConfig()) { @@ -154,6 +124,7 @@ private function getCloudflareCacheProxy(): CloudflareProxyCache /** * @param array $body * @return Request + * @throws \JsonException */ protected function createRequest(array $body): Request { @@ -169,16 +140,18 @@ protected function createRequest(array $body): Request $this->getCloudflareCacheProxy()->getVersion(), $this->getCloudflareCacheProxy()->getZone() ); + $body = \json_encode($body, JSON_THROW_ON_ERROR); return new Request( 'POST', $uri, $headers, - \json_encode($body) + $body ); } /** * @return Request + * @throws \JsonException */ protected function createBanRequest(): Request { @@ -189,8 +162,8 @@ protected function createBanRequest(): Request /** * @param string[] $uris - * * @return Request + * @throws \JsonException */ protected function createPurgeRequest(array $uris = []): Request { diff --git a/src/EventSubscriber/LocaleSubscriber.php b/src/EventSubscriber/LocaleSubscriber.php index cfdd022b..8583550f 100644 --- a/src/EventSubscriber/LocaleSubscriber.php +++ b/src/EventSubscriber/LocaleSubscriber.php @@ -7,19 +7,20 @@ use Doctrine\Persistence\ManagerRegistry; use RZ\Roadiz\Core\AbstractEntities\TranslationInterface; use RZ\Roadiz\CoreBundle\Entity\Translation; +use RZ\Roadiz\CoreBundle\Preview\PreviewResolverInterface; +use RZ\Roadiz\CoreBundle\Repository\TranslationRepository; use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\Routing\RequestContextAwareInterface; final class LocaleSubscriber implements EventSubscriberInterface { - private ManagerRegistry $managerRegistry; - private RequestContextAwareInterface $router; - - public function __construct(ManagerRegistry $managerRegistry, RequestContextAwareInterface $router) - { - $this->managerRegistry = $managerRegistry; - $this->router = $router; + public function __construct( + private readonly PreviewResolverInterface $previewResolver, + private readonly ManagerRegistry $managerRegistry, + private readonly RequestContextAwareInterface $router + ) { } /** @@ -29,13 +30,44 @@ public static function getSubscribedEvents(): array { return [ // must be registered just after Symfony\Component\HttpKernel\EventListener\LocaleListener - RequestEvent::class => [['onKernelRequest', 16]], + RequestEvent::class => ['onKernelRequest', 16], ]; } + private function getRepository(): TranslationRepository + { + return $this->managerRegistry->getRepository(Translation::class); + } + private function getDefaultTranslation(): ?TranslationInterface { - return $this->managerRegistry->getRepository(Translation::class)->findDefault(); + return $this->getRepository()->findDefault(); + } + + private function supportsLocale(?string $locale): bool + { + if (null === $locale || $locale === '') { + return false; + } + + if ($this->previewResolver->isPreview()) { + $locales = $this->getRepository()->getAllLocales(); + } else { + $locales = $this->getRepository()->getAvailableLocales(); + } + return \in_array( + $locale, + $locales, + true + ); + } + + private function getTranslationByLocale(string $locale): ?TranslationInterface + { + if ($this->previewResolver->isPreview()) { + return $this->getRepository()->findOneByLocaleOrOverrideLocale($locale); + } + return $this->getRepository()->findOneAvailableByLocaleOrOverrideLocale($locale); } public function onKernelRequest(RequestEvent $event): void @@ -43,27 +75,53 @@ public function onKernelRequest(RequestEvent $event): void $request = $event->getRequest(); $locale = $request->query->get('_locale') ?? $request->attributes->get('_locale'); - if ($request->hasPreviousSession()) { - $locale = $request->getSession()->get('_locale', null); - if (null !== $locale) { - $this->setLocale($event, $locale); - } - } - /* * Set default locale */ - if (null !== $locale && $locale !== '') { - $this->setLocale($event, $locale); - } elseif (null !== $translation = $this->getDefaultTranslation()) { - $shortLocale = $translation->getLocale(); - $this->setLocale($event, $shortLocale); + if ($this->supportsLocale($locale)) { + $this->setTranslation($request, $this->getTranslationByLocale($locale)); + return; + } + + $statelessRoutes = [ + 'api_genid', + 'api_doc', + 'api_entrypoint', + 'api_graphql_entrypoint', + 'api_jsonld_context', + 'healthCheckAction', + 'interventionRequestProcess', + ]; + if ( + !\in_array($request->attributes->getString('_route'), $statelessRoutes, true) && + !$request->attributes->getBoolean('_stateless') && + $request->hasPreviousSession() + ) { + $sessionLocale = $request->getSession()->get('_locale', null); + if ($this->supportsLocale($sessionLocale)) { + $this->setTranslation($request, $this->getTranslationByLocale($sessionLocale)); + return; + } + } + + if (null !== $translation = $this->getDefaultTranslation()) { + $this->setTranslation($request, $translation); + return; } } - private function setLocale(RequestEvent $event, string $locale): void + private function setTranslation(Request $request, ?TranslationInterface $translation): void { - $event->getRequest()->setLocale($locale); + if (null === $translation) { + return; + } + $locale = $translation->getPreferredLocale(); + /* + * Set current translation globally for controllers, utils, etc + */ + $request->attributes->set('_translation', $translation); + $request->attributes->set('_locale', $locale); + $request->setLocale($locale); \Locale::setDefault($locale); $this->router->getContext()->setParameter('_locale', $locale); } diff --git a/src/EventSubscriber/LoggableSubscriber.php b/src/EventSubscriber/LoggableSubscriber.php index 9dc9a7ed..8bec9f86 100644 --- a/src/EventSubscriber/LoggableSubscriber.php +++ b/src/EventSubscriber/LoggableSubscriber.php @@ -5,27 +5,18 @@ namespace RZ\Roadiz\CoreBundle\EventSubscriber; use Gedmo\Loggable\LoggableListener; +use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\KernelEvents; -use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; -use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; final class LoggableSubscriber implements EventSubscriberInterface { - private LoggableListener $loggableListener; - private ?TokenStorageInterface $tokenStorage; - private ?AuthorizationCheckerInterface $authorizationChecker; - public function __construct( - LoggableListener $loggableListener, - TokenStorageInterface $tokenStorage = null, - AuthorizationCheckerInterface $authorizationChecker = null + private readonly LoggableListener $loggableListener, + private readonly Security $security, ) { - $this->loggableListener = $loggableListener; - $this->tokenStorage = $tokenStorage; - $this->authorizationChecker = $authorizationChecker; } public function onKernelRequest(RequestEvent $event): void @@ -34,14 +25,12 @@ public function onKernelRequest(RequestEvent $event): void return; } - if (null === $this->tokenStorage || null === $this->authorizationChecker) { + if (null === $user = $this->security->getUser()) { return; } - $token = $this->tokenStorage->getToken(); - - if (null !== $token && $this->authorizationChecker->isGranted('IS_AUTHENTICATED_REMEMBERED')) { - $this->loggableListener->setUsername($token); + if ($this->security->isGranted('IS_AUTHENTICATED_REMEMBERED')) { + $this->loggableListener->setUsername($user); } } diff --git a/src/EventSubscriber/NodeDuplicationSubscriber.php b/src/EventSubscriber/NodeDuplicationSubscriber.php index 7370ee3d..6990f179 100644 --- a/src/EventSubscriber/NodeDuplicationSubscriber.php +++ b/src/EventSubscriber/NodeDuplicationSubscriber.php @@ -10,26 +10,18 @@ use RZ\Roadiz\CoreBundle\EntityHandler\NodeHandler; use Symfony\Component\EventDispatcher\EventSubscriberInterface; -class NodeDuplicationSubscriber implements EventSubscriberInterface +final class NodeDuplicationSubscriber implements EventSubscriberInterface { - protected HandlerFactoryInterface $handlerFactory; - private ManagerRegistry $managerRegistry; - - /** - * @param ManagerRegistry $managerRegistry - * @param HandlerFactoryInterface $handlerFactory - */ - public function __construct(ManagerRegistry $managerRegistry, HandlerFactoryInterface $handlerFactory) - { - $this->handlerFactory = $handlerFactory; - $this->managerRegistry = $managerRegistry; + public function __construct( + private readonly ManagerRegistry $managerRegistry, + private readonly HandlerFactoryInterface $handlerFactory + ) { } public static function getSubscribedEvents(): array { return [ NodeDuplicatedEvent::class => 'cleanPosition', - '\RZ\Roadiz\Core\Events\Node\NodeDuplicatedEvent' => 'cleanPosition', ]; } diff --git a/src/EventSubscriber/NodeNameSubscriber.php b/src/EventSubscriber/NodeNameSubscriber.php index 17517016..91659409 100644 --- a/src/EventSubscriber/NodeNameSubscriber.php +++ b/src/EventSubscriber/NodeNameSubscriber.php @@ -43,7 +43,6 @@ public static function getSubscribedEvents(): array { return [ NodesSourcesPreUpdatedEvent::class => ['onBeforeUpdate', 0], - '\RZ\Roadiz\Core\Events\NodesSources\NodesSourcesPreUpdatedEvent' => ['onBeforeUpdate', 0], ]; } diff --git a/src/EventSubscriber/NodeRedirectionSubscriber.php b/src/EventSubscriber/NodeRedirectionSubscriber.php index 31158fbb..2b3e00e8 100644 --- a/src/EventSubscriber/NodeRedirectionSubscriber.php +++ b/src/EventSubscriber/NodeRedirectionSubscriber.php @@ -10,37 +10,23 @@ use RZ\Roadiz\CoreBundle\Preview\PreviewResolverInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; -use Symfony\Component\HttpKernel\KernelInterface; /** * Subscribe to Node, NodesSources and UrlAlias event to clear ns url cache. */ class NodeRedirectionSubscriber implements EventSubscriberInterface { - protected NodeMover $nodeMover; - protected KernelInterface $kernel; - protected PreviewResolverInterface $previewResolver; - - /** - * @param NodeMover $nodeMover - * @param KernelInterface $kernel - * @param PreviewResolverInterface $previewResolver - */ public function __construct( - NodeMover $nodeMover, - KernelInterface $kernel, - PreviewResolverInterface $previewResolver + protected readonly NodeMover $nodeMover, + protected readonly string $kernelEnvironment, + protected readonly PreviewResolverInterface $previewResolver ) { - $this->nodeMover = $nodeMover; - $this->kernel = $kernel; - $this->previewResolver = $previewResolver; } public static function getSubscribedEvents(): array { return [ NodePathChangedEvent::class => 'redirectOldPaths', - '\RZ\Roadiz\Core\Events\Node\NodePathChangedEvent' => 'redirectOldPaths' ]; } @@ -57,7 +43,7 @@ public function redirectOldPaths( EventDispatcherInterface $dispatcher ): void { if ( - $this->kernel->getEnvironment() === 'prod' && + $this->kernelEnvironment === 'prod' && !$this->previewResolver->isPreview() && null !== $event->getNode() && $event->getNode()->isPublished() && diff --git a/src/EventSubscriber/NodeSourcePathSubscriber.php b/src/EventSubscriber/NodeSourcePathSubscriber.php index 82dbaccf..0165266b 100644 --- a/src/EventSubscriber/NodeSourcePathSubscriber.php +++ b/src/EventSubscriber/NodeSourcePathSubscriber.php @@ -9,16 +9,11 @@ use RZ\Roadiz\CoreBundle\Routing\NodesSourcesUrlGenerator; use Symfony\Component\EventDispatcher\EventSubscriberInterface; -class NodeSourcePathSubscriber implements EventSubscriberInterface +final class NodeSourcePathSubscriber implements EventSubscriberInterface { - protected NodesSourcesPathAggregator $pathAggregator; - - /** - * @param NodesSourcesPathAggregator $pathAggregator - */ - public function __construct(NodesSourcesPathAggregator $pathAggregator) - { - $this->pathAggregator = $pathAggregator; + public function __construct( + private readonly NodesSourcesPathAggregator $pathAggregator + ) { } /** @@ -27,8 +22,7 @@ public function __construct(NodesSourcesPathAggregator $pathAggregator) public static function getSubscribedEvents(): array { return [ - NodesSourcesPathGeneratingEvent::class => [['onNodesSourcesPath', -100]], - '\RZ\Roadiz\Core\Events\NodesSources\NodesSourcesPathGeneratingEvent' => [['onNodesSourcesPath', -100]], + NodesSourcesPathGeneratingEvent::class => ['onNodesSourcesPath', -100], ]; } @@ -39,7 +33,6 @@ public function onNodesSourcesPath(NodesSourcesPathGeneratingEvent $event): void { $urlGenerator = new NodesSourcesUrlGenerator( $this->pathAggregator, - null, $event->getNodeSource(), $event->isForceLocale(), $event->isForceLocaleWithUrlAlias() diff --git a/src/EventSubscriber/NodesSourcesAddHeadersSubscriber.php b/src/EventSubscriber/NodesSourcesAddHeadersSubscriber.php new file mode 100644 index 00000000..63808c9f --- /dev/null +++ b/src/EventSubscriber/NodesSourcesAddHeadersSubscriber.php @@ -0,0 +1,86 @@ + ['onKernelResponse', 0] + ]; + } + + public function onKernelResponse(ResponseEvent $event): void + { + $request = $event->getRequest(); + $response = $event->getResponse(); + + if (!$request->isMethodCacheable()) { + return; + } + if (!$response->getContent() || !$response->isSuccessful()) { + return; + } + + $attributes = RequestAttributesExtractor::extractAttributes($request); + if (\count($attributes) < 1) { + return; + } + + if ($this->previewResolver->isPreview()) { + return; + } + + if ($this->security->isGranted('IS_AUTHENTICATED')) { + return; + } + + $resourceCacheHeaders = $attributes['cache_headers'] ?? []; + $data = $request->attributes->get('data'); + + // Work with WebResponse item instead of WebResponse itself + if ($data instanceof WebResponseInterface) { + $data = $data->getItem(); + } + + // if the public-property is defined and not yet set; apply it to the response + $public = $resourceCacheHeaders['public'] ?? null; + if (null !== $public && !$response->headers->hasCacheControlDirective('public')) { + $public ? $response->setPublic() : $response->setPrivate(); + } + + if (!$data instanceof NodesSources) { + return; + } + + if ($data->getNode()->getTtl() <= 0) { + return; + } + + if (null !== ($maxAge = $resourceCacheHeaders['max_age'] ?? $data->getNode()->getTtl()) && !$response->headers->hasCacheControlDirective('max-age')) { + $response->setMaxAge($maxAge * 60); + } + // Cache-Control "s-maxage" is only relevant is resource is not marked as "private" + if (false !== $public && null !== ($sharedMaxAge = $resourceCacheHeaders['shared_max_age'] ?? $data->getNode()->getTtl()) && !$response->headers->hasCacheControlDirective('s-maxage')) { + $response->setSharedMaxAge($sharedMaxAge * 60); + } + } +} diff --git a/src/EventSubscriber/NodesSourcesLinkHeaderEventSubscriber.php b/src/EventSubscriber/NodesSourcesLinkHeaderEventSubscriber.php index a852c6eb..3c0b5c91 100644 --- a/src/EventSubscriber/NodesSourcesLinkHeaderEventSubscriber.php +++ b/src/EventSubscriber/NodesSourcesLinkHeaderEventSubscriber.php @@ -6,6 +6,7 @@ use Doctrine\Persistence\ManagerRegistry; use Psr\Link\EvolvableLinkProviderInterface; +use RZ\Roadiz\CoreBundle\Api\Model\WebResponseInterface; use RZ\Roadiz\CoreBundle\Entity\NodesSources; use Symfony\Cmf\Component\Routing\RouteObjectInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; @@ -14,17 +15,12 @@ use Symfony\Component\WebLink\GenericLinkProvider; use Symfony\Component\WebLink\Link; -class NodesSourcesLinkHeaderEventSubscriber implements EventSubscriberInterface +final class NodesSourcesLinkHeaderEventSubscriber implements EventSubscriberInterface { - private ManagerRegistry $managerRegistry; - private UrlGeneratorInterface $urlGenerator; - public function __construct( - ManagerRegistry $managerRegistry, - UrlGeneratorInterface $urlGenerator + private readonly ManagerRegistry $managerRegistry, + private readonly UrlGeneratorInterface $urlGenerator ) { - $this->managerRegistry = $managerRegistry; - $this->urlGenerator = $urlGenerator; } /** @@ -40,33 +36,40 @@ public static function getSubscribedEvents(): array public function onKernelView(ViewEvent $event): void { $request = $event->getRequest(); - $resources = $request->attributes->get('data', null); + $resources = $request->attributes->get('data'); $linkProvider = $request->attributes->get('_links', new GenericLinkProvider()); - if ($resources instanceof NodesSources && $linkProvider instanceof EvolvableLinkProviderInterface) { - /* - * Preview and authentication is handled at repository level. - */ - /** @var NodesSources[] $allSources */ - $allSources = $this->managerRegistry - ->getRepository(get_class($resources)) - ->findByNode($resources->getNode()); + // Work with WebResponse item instead of WebResponse itself + if ($resources instanceof WebResponseInterface) { + $resources = $resources->getItem(); + } + + if (!$resources instanceof NodesSources || !$linkProvider instanceof EvolvableLinkProviderInterface) { + return; + } + + /* + * Preview and authentication is handled at repository level. + */ + /** @var NodesSources[] $allSources */ + $allSources = $this->managerRegistry + ->getRepository(get_class($resources)) + ->findByNode($resources->getNode()); - foreach ($allSources as $singleSource) { - $linkProvider = $linkProvider->withLink( - (new Link( - 'alternate', - $this->urlGenerator->generate(RouteObjectInterface::OBJECT_BASED_ROUTE_NAME, [ - RouteObjectInterface::ROUTE_OBJECT => $singleSource - ]) - )) - ->withAttribute('hreflang', $singleSource->getTranslation()->getLocale()) - // Must encode translation name in base64 because headers are ASCII only - ->withAttribute('title', \base64_encode($singleSource->getTranslation()->getName())) - ->withAttribute('type', 'text/html') - ); - } - $request->attributes->set('_links', $linkProvider); + foreach ($allSources as $singleSource) { + $linkProvider = $linkProvider->withLink( + (new Link( + 'alternate', + $this->urlGenerator->generate(RouteObjectInterface::OBJECT_BASED_ROUTE_NAME, [ + RouteObjectInterface::ROUTE_OBJECT => $singleSource + ]) + )) + ->withAttribute('hreflang', $singleSource->getTranslation()->getLocale()) + // Must encode translation name in base64 because headers are ASCII only + ->withAttribute('title', \base64_encode($singleSource->getTranslation()->getName())) + ->withAttribute('type', 'text/html') + ); } + $request->attributes->set('_links', $linkProvider); } } diff --git a/src/EventSubscriber/NodesSourcesUniversalSubscriber.php b/src/EventSubscriber/NodesSourcesUniversalSubscriber.php index d13e80d6..b16cb180 100644 --- a/src/EventSubscriber/NodesSourcesUniversalSubscriber.php +++ b/src/EventSubscriber/NodesSourcesUniversalSubscriber.php @@ -33,7 +33,6 @@ public static function getSubscribedEvents(): array { return [ NodesSourcesUpdatedEvent::class => 'duplicateUniversalContents', - '\RZ\Roadiz\Core\Events\NodesSources\NodesSourcesUpdatedEvent' => 'duplicateUniversalContents', ]; } diff --git a/src/EventSubscriber/NodesSourcesUrlsCacheEventSubscriber.php b/src/EventSubscriber/NodesSourcesUrlsCacheEventSubscriber.php index 771eea51..19a51cbd 100644 --- a/src/EventSubscriber/NodesSourcesUrlsCacheEventSubscriber.php +++ b/src/EventSubscriber/NodesSourcesUrlsCacheEventSubscriber.php @@ -37,28 +37,17 @@ public static function getSubscribedEvents(): array { return [ NodesSourcesCreatedEvent::class => 'onPurgeRequest', - '\RZ\Roadiz\Core\Events\NodesSources\NodesSourcesCreatedEvent' => 'onPurgeRequest', NodesSourcesDeletedEvent::class => 'onPurgeRequest', - '\RZ\Roadiz\Core\Events\NodesSources\NodesSourcesDeletedEvent' => 'onPurgeRequest', TranslationUpdatedEvent::class => 'onPurgeRequest', - '\RZ\Roadiz\Core\Events\Translation\TranslationUpdatedEvent' => 'onPurgeRequest', TranslationDeletedEvent::class => 'onPurgeRequest', - '\RZ\Roadiz\Core\Events\Translation\TranslationDeletedEvent' => 'onPurgeRequest', NodeDeletedEvent::class => 'onPurgeRequest', - '\RZ\Roadiz\Core\Events\Node\NodeDeletedEvent' => 'onPurgeRequest', NodeUndeletedEvent::class => 'onPurgeRequest', - '\RZ\Roadiz\Core\Events\Node\NodeUndeletedEvent' => 'onPurgeRequest', NodeUpdatedEvent::class => 'onPurgeRequest', - '\RZ\Roadiz\Core\Events\Node\NodeUpdatedEvent' => 'onPurgeRequest', UrlAliasCreatedEvent::class => 'onPurgeRequest', - '\RZ\Roadiz\Core\Events\UrlAlias\UrlAliasCreatedEvent' => 'onPurgeRequest', UrlAliasUpdatedEvent::class => 'onPurgeRequest', - '\RZ\Roadiz\Core\Events\UrlAlias\UrlAliasUpdatedEvent' => 'onPurgeRequest', UrlAliasDeletedEvent::class => 'onPurgeRequest', - '\RZ\Roadiz\Core\Events\UrlAlias\UrlAliasDeletedEvent' => 'onPurgeRequest', 'workflow.node.completed' => 'onPurgeRequest', CachePurgeRequestEvent::class => ['onPurgeRequest', 3], - '\RZ\Roadiz\Core\Events\Cache\CachePurgeRequestEvent' => ['onPurgeRequest', 3], ]; } diff --git a/src/EventSubscriber/OPCacheEventSubscriber.php b/src/EventSubscriber/OPCacheEventSubscriber.php index 6ab73e53..fb080645 100644 --- a/src/EventSubscriber/OPCacheEventSubscriber.php +++ b/src/EventSubscriber/OPCacheEventSubscriber.php @@ -17,7 +17,6 @@ public static function getSubscribedEvents(): array { return [ CachePurgeRequestEvent::class => ['onPurgeRequest', 3], - '\RZ\Roadiz\Core\Events\Cache\CachePurgeRequestEvent' => ['onPurgeRequest', 3], ]; } diff --git a/src/EventSubscriber/RealmNodeInheritanceSubscriber.php b/src/EventSubscriber/RealmNodeInheritanceSubscriber.php index b18da0fa..22ffbd6f 100644 --- a/src/EventSubscriber/RealmNodeInheritanceSubscriber.php +++ b/src/EventSubscriber/RealmNodeInheritanceSubscriber.php @@ -59,7 +59,7 @@ public function onNodeJoinedRealm(AbstractRealmNodeEvent $event): void */ $this->bus->dispatch(new Envelope(new ApplyRealmNodeInheritanceMessage( $event->getRealmNode()->getNode()->getId(), - $event->getRealmNode()->getRealm()?->getId() + $event->getRealmNode()->getRealm()->getId() ))); } @@ -70,7 +70,7 @@ public function onNodeLeftRealm(AbstractRealmNodeEvent $event): void */ $this->bus->dispatch(new Envelope(new CleanRealmNodeInheritanceMessage( $event->getRealmNode()->getNode()->getId(), - $event->getRealmNode()->getRealm()?->getId() + $event->getRealmNode()->getRealm()->getId() ))); } } diff --git a/src/EventSubscriber/RedirectionCacheSubscriber.php b/src/EventSubscriber/RedirectionCacheSubscriber.php new file mode 100644 index 00000000..49dece5c --- /dev/null +++ b/src/EventSubscriber/RedirectionCacheSubscriber.php @@ -0,0 +1,40 @@ +cacheAdapter = $cacheAdapter; + } + + /** + * @inheritDoc + */ + public static function getSubscribedEvents(): array + { + return [ + PostCreatedRedirectionEvent::class => 'clearCache', + PostDeletedRedirectionEvent::class => 'clearCache', + PostUpdatedRedirectionEvent::class => 'clearCache', + ]; + } + + public function clearCache(RedirectionEvent $event): void + { + $this->cacheAdapter->deleteItem(RedirectionPathResolver::CACHE_KEY); + } +} diff --git a/src/EventSubscriber/ReverseProxyCacheEventSubscriber.php b/src/EventSubscriber/ReverseProxyCacheEventSubscriber.php index eec69e0c..d59055fc 100644 --- a/src/EventSubscriber/ReverseProxyCacheEventSubscriber.php +++ b/src/EventSubscriber/ReverseProxyCacheEventSubscriber.php @@ -46,9 +46,7 @@ public static function getSubscribedEvents(): array { return [ CachePurgeRequestEvent::class => ['onBanRequest', 3], - '\RZ\Roadiz\Core\Events\Cache\CachePurgeRequestEvent' => ['onBanRequest', 3], NodesSourcesUpdatedEvent::class => ['onPurgeRequest', 3], - '\RZ\Roadiz\Core\Events\NodesSources\NodesSourcesUpdatedEvent' => ['onPurgeRequest', 3], 'workflow.node.completed' => ['onNodeWorkflowCompleted', 3], ]; } @@ -115,9 +113,16 @@ protected function createBanRequests(): array { $requests = []; foreach ($this->reverseProxyCacheLocator->getFrontends() as $frontend) { + // Add protocol if host does not start with it + if (!\str_starts_with($frontend->getHost(), 'http')) { + // Use HTTP to be able to call Varnish from a Docker network + $uri = 'http://' . $frontend->getHost(); + } else { + $uri = $frontend->getHost(); + } $requests[$frontend->getName()] = new Request( 'BAN', - 'http://' . $frontend->getHost(), + $uri, [ 'Host' => $frontend->getDomainName() ] diff --git a/src/EventSubscriber/RoleSubscriber.php b/src/EventSubscriber/RoleSubscriber.php index d04eb2ab..b3547449 100644 --- a/src/EventSubscriber/RoleSubscriber.php +++ b/src/EventSubscriber/RoleSubscriber.php @@ -37,11 +37,8 @@ public static function getSubscribedEvents(): array { return [ PreCreatedRoleEvent::class => 'onRoleChanged', - '\RZ\Roadiz\Core\Events\Role\PreCreatedRoleEvent' => 'onRoleChanged', PreUpdatedRoleEvent::class => 'onRoleChanged', - '\RZ\Roadiz\Core\Events\Role\PreUpdatedRoleEvent' => 'onRoleChanged', PreDeletedRoleEvent::class => 'onRoleChanged', - '\RZ\Roadiz\Core\Events\Role\PreDeletedRoleEvent' => 'onRoleChanged', ]; } diff --git a/src/EventSubscriber/SignatureSubscriber.php b/src/EventSubscriber/SignatureSubscriber.php index 518e73b1..68bcb97c 100644 --- a/src/EventSubscriber/SignatureSubscriber.php +++ b/src/EventSubscriber/SignatureSubscriber.php @@ -10,15 +10,11 @@ final class SignatureSubscriber implements EventSubscriberInterface { - private string $version; - private bool $debug; - private bool $hideRoadizVersion; - - public function __construct(string $cmsVersion, bool $hideRoadizVersion, bool $debug = false) - { - $this->version = $cmsVersion; - $this->debug = $debug; - $this->hideRoadizVersion = $hideRoadizVersion; + public function __construct( + private readonly string $cmsVersion, + private readonly bool $hideRoadizVersion, + private readonly bool $debug = false + ) { } /** * Filters the Response. @@ -34,8 +30,8 @@ public function onKernelResponse(ResponseEvent $event): void $response = $event->getResponse(); $response->headers->add(['X-Powered-By' => 'Roadiz CMS']); - if ($this->debug && $this->version) { - $response->headers->add(['X-Version' => $this->version]); + if ($this->debug && $this->cmsVersion) { + $response->headers->add(['X-Version' => $this->cmsVersion]); } } diff --git a/src/EventSubscriber/TagTimestampSubscriber.php b/src/EventSubscriber/TagTimestampSubscriber.php index ca59a717..86b30b43 100644 --- a/src/EventSubscriber/TagTimestampSubscriber.php +++ b/src/EventSubscriber/TagTimestampSubscriber.php @@ -22,9 +22,6 @@ public static function getSubscribedEvents(): array public function onTagUpdatedEvent(TagUpdatedEvent $event): void { - $tag = $event->getTag(); - if ($tag instanceof AbstractDateTimed) { - $tag->setUpdatedAt(new \DateTime()); - } + $event->getTag()->setUpdatedAt(new \DateTime()); } } diff --git a/src/EventSubscriber/TranslationSubscriber.php b/src/EventSubscriber/TranslationSubscriber.php index e7f61509..d9b85f68 100644 --- a/src/EventSubscriber/TranslationSubscriber.php +++ b/src/EventSubscriber/TranslationSubscriber.php @@ -18,24 +18,18 @@ /** * Subscribe to Translation event to clear result cache. */ -class TranslationSubscriber implements EventSubscriberInterface +final class TranslationSubscriber implements EventSubscriberInterface { - protected ManagerRegistry $managerRegistry; - - public function __construct(ManagerRegistry $managerRegistry) + public function __construct(private readonly ManagerRegistry $managerRegistry) { - $this->managerRegistry = $managerRegistry; } public static function getSubscribedEvents(): array { return [ TranslationCreatedEvent::class => 'purgeCache', - '\RZ\Roadiz\Core\Events\Translation\TranslationCreatedEvent' => 'purgeCache', TranslationUpdatedEvent::class => 'purgeCache', - '\RZ\Roadiz\Core\Events\Translation\TranslationUpdatedEvent' => 'purgeCache', TranslationDeletedEvent::class => 'purgeCache', - '\RZ\Roadiz\Core\Events\Translation\TranslationDeletedEvent' => 'purgeCache', ]; } diff --git a/src/EventSubscriber/UserLocaleSubscriber.php b/src/EventSubscriber/UserLocaleSubscriber.php index fc75e89f..5b865a5c 100644 --- a/src/EventSubscriber/UserLocaleSubscriber.php +++ b/src/EventSubscriber/UserLocaleSubscriber.php @@ -15,15 +15,10 @@ final class UserLocaleSubscriber implements EventSubscriberInterface { - private RequestStack $requestStack; - private TokenStorageInterface $tokenStorage; - public function __construct( - RequestStack $requestStack, - TokenStorageInterface $tokenStorage + private readonly RequestStack $requestStack, + private readonly TokenStorageInterface $tokenStorage ) { - $this->requestStack = $requestStack; - $this->tokenStorage = $tokenStorage; } /** @@ -34,8 +29,7 @@ public static function getSubscribedEvents(): array // must be registered after the default Locale listener return [ SecurityEvents::INTERACTIVE_LOGIN => 'onInteractiveLogin', - UserUpdatedEvent::class => [['onUserUpdated']], - '\RZ\Roadiz\Core\Events\User\UserUpdatedEvent' => [['onUserUpdated']], + UserUpdatedEvent::class => 'onUserUpdated', ]; } @@ -44,6 +38,10 @@ public static function getSubscribedEvents(): array */ public function onInteractiveLogin(InteractiveLoginEvent $event): void { + if ($this->requestStack->getMainRequest()?->attributes->getBoolean('_stateless')) { + return; + } + $user = $event->getAuthenticationToken()->getUser(); if ( @@ -59,12 +57,15 @@ public function onInteractiveLogin(InteractiveLoginEvent $event): void */ public function onUserUpdated(FilterUserEvent $event): void { + if ($this->requestStack->getMainRequest()?->attributes->getBoolean('_stateless')) { + return; + } $user = $event->getUser(); if ( null !== $this->tokenStorage->getToken() && $this->tokenStorage->getToken()->getUser() instanceof User && - $this->tokenStorage->getToken()->getUsername() === $user->getUsername() + $this->tokenStorage->getToken()->getUserIdentifier() === $user->getUserIdentifier() ) { if (null === $user->getLocale()) { $this->requestStack->getSession()->remove('_locale'); diff --git a/src/Exception/EmptySaltException.php b/src/Exception/EmptySaltException.php deleted file mode 100644 index 8ebc655b..00000000 --- a/src/Exception/EmptySaltException.php +++ /dev/null @@ -1,12 +0,0 @@ -foreground_colors['black'] = '0;30'; - $this->foreground_colors['dark_gray'] = '1;30'; - $this->foreground_colors['blue'] = '0;34'; - $this->foreground_colors['light_blue'] = '1;34'; - $this->foreground_colors['green'] = '0;32'; - $this->foreground_colors['light_green'] = '1;32'; - $this->foreground_colors['cyan'] = '0;36'; - $this->foreground_colors['light_cyan'] = '1;36'; - $this->foreground_colors['red'] = '0;31'; - $this->foreground_colors['light_red'] = '1;31'; - $this->foreground_colors['purple'] = '0;35'; - $this->foreground_colors['light_purple'] = '1;35'; - $this->foreground_colors['brown'] = '0;33'; - $this->foreground_colors['yellow'] = '1;33'; - $this->foreground_colors['light_gray'] = '0;37'; - $this->foreground_colors['white'] = '1;37'; - - $this->background_colors['black'] = '40'; - $this->background_colors['red'] = '41'; - $this->background_colors['green'] = '42'; - $this->background_colors['yellow'] = '43'; - $this->background_colors['blue'] = '44'; - $this->background_colors['magenta'] = '45'; - $this->background_colors['cyan'] = '46'; - $this->background_colors['light_gray'] = '47'; - } - - /** - * @param \Exception|\TypeError $exception - * @return int - */ - public function getHttpStatusCode($exception): int - { - if ($exception instanceof HttpExceptionInterface) { - return $exception->getStatusCode(); - } elseif ($exception instanceof ResourceNotFoundException) { - return Response::HTTP_NOT_FOUND; - } elseif ($exception instanceof MaintenanceModeException) { - return Response::HTTP_SERVICE_UNAVAILABLE; - } elseif ($exception instanceof AccessDeniedException || $exception instanceof AccessDeniedHttpException) { - return Response::HTTP_FORBIDDEN; - } - - return Response::HTTP_INTERNAL_SERVER_ERROR; - } - - /** - * @param \Exception|\TypeError $e - * @return string - */ - public function getHumanExceptionTitle($e): string - { - if ($e instanceof MaintenanceModeException) { - return "Website is under maintenance."; - } - - if ($e instanceof NoConfigurationFoundException) { - return "No configuration file has been found. Did you run composer install before using Roadiz?"; - } - - if ($e instanceof InvalidConfigurationException) { - return "Roadiz configuration is not valid."; - } - - if ($e instanceof ResourceNotFoundException || $e instanceof NotFoundHttpException) { - return "Resource not found."; - } - - if ($e instanceof ConnectionException || $e instanceof \Doctrine\DBAL\ConnectionException) { - return "Your database is not reachable. Did you run install before using Roadiz?"; - } - - if ($e instanceof TableNotFoundException) { - return "Your database is not synchronised to Roadiz data schema. Did you run install before using Roadiz?"; - } - - if ($e instanceof AccessDeniedException || $e instanceof AccessDeniedHttpException) { - return "Oups! Wrong way, you are not supposed to be here."; - } - - return "A problem occurred on our website. We are working on this to be back soon."; - } - - /** - * @param \Exception|\TypeError $e - * @return string - */ - public function getJsonError($e): string - { - if ($e instanceof NoConfigurationFoundException) { - return "no_configuration_file"; - } - - if ($e instanceof InvalidConfigurationException) { - return "invalid_configuration"; - } - - if ($e instanceof ResourceNotFoundException || $e instanceof NotFoundHttpException) { - return "not_found"; - } - - if ($e instanceof ConnectionException || $e instanceof \Doctrine\DBAL\ConnectionException) { - return "database_not_reachable"; - } - - if ($e instanceof TableNotFoundException) { - return "database_not_uptodate"; - } - - if ($e instanceof AccessDeniedException || $e instanceof AccessDeniedHttpException) { - return "access_denied"; - } - - return "general_error"; - } - - - /** - * @param \Exception|\TypeError $e - * @param Request $request - * @param bool $debug - * @return JsonResponse|Response - */ - public function getResponse($e, Request $request, bool $debug = false): Response - { - /* - * Log error before displaying a fallback page. - */ - $class = get_class($e); - - $humanMessage = $this->getHumanExceptionTitle($e); - - if (php_sapi_name() === 'cli') { - return new Response( - implode(PHP_EOL, [ - $this->getColoredString('[' . $class . ']', 'white', 'red'), - $this->getColoredString($e->getMessage(), 'red', null), - ]) . PHP_EOL, - $this->getHttpStatusCode($e), - [ - 'content-type' => 'text/plain', - ] - ); - } elseif ($this->isFormatJson($request)) { - $data = [ - 'error' => $this->getJsonError($e), - 'error_message' => $e->getMessage(), - 'message' => $e->getMessage(), - 'exception' => $class, - 'humanMessage' => $humanMessage, - 'status' => $this->getHttpStatusCode($e), - ]; - if ($debug) { - $data['error_trace'] = $e->getTrace(); - } - return new JsonResponse($data, $this->getHttpStatusCode($e)); - } else { - $html = file_get_contents(dirname(__DIR__) . '/../templates/emerg.html'); - $html = str_replace('{{ http_code }}', (string) $this->getHttpStatusCode($e), $html); - $html = str_replace('{{ human_message }}', $humanMessage, $html); - - if ($e instanceof MaintenanceModeException) { - $html = str_replace('{{ smiley }}', '🏗', $html); - } elseif ($this->getHttpStatusCode($e) === Response::HTTP_FORBIDDEN) { - $html = str_replace('{{ smiley }}', '🤔', $html); - } elseif ($this->getHttpStatusCode($e) === Response::HTTP_NOT_FOUND) { - $html = str_replace('{{ smiley }}', '🧐', $html); - } else { - $html = str_replace('{{ smiley }}', '🤕', $html); - } - - if ($debug) { - $html = str_replace('{{ message }}', $e->getMessage(), $html); - $trace = preg_replace('#([^\n]+)#', '

$1

', $e->getTraceAsString()); - $trace = $this->addTwigSource($e, $trace); - $html = str_replace('{{ details }}', $trace, $html); - $html = str_replace('{{ exception }}', $class, $html); - } else { - $html = str_replace('{{ message }}', '', $html); - $html = str_replace('{{ details }}', '', $html); - $html = str_replace('{{ exception }}', '', $html); - } - - return new Response( - $html, - $this->getHttpStatusCode($e), - [ - 'content-type' => 'text/html', - 'X-Error-Reason' => $e->getMessage(), - ] - ); - } - } - - /** - * @param \Exception|\TypeError $e - * @param string $trace - * - * @return string - */ - protected function addTwigSource($e, string $trace): string - { - if ($e instanceof SyntaxError && null !== $e->getSourceContext()) { - return '' . PHP_EOL . - '' . PHP_EOL . - '
Template' . $e->getSourceContext()->getName() . '
Line number' . $e->getTemplateLine() . '
Path' . $e->getSourceContext()->getPath() . '
' . PHP_EOL . - $trace; - } elseif ($e instanceof Error && null !== $e->getSourceContext()) { - return '' . PHP_EOL . - '
Template' . $e->getSourceContext()->getName() . '
' . $e->getSourceContext()->getPath() . '
' . PHP_EOL . - $trace; - } - return $trace; - } - - /** - * @param Request $request - * @return bool - */ - public function isFormatJson(Request $request): bool - { - if ( - $request->attributes->has('_format') && - ( - $request->attributes->get('_format') == 'json' || - $request->attributes->get('_format') == 'ld+json' - ) - ) { - return true; - } - - if ( - $request->headers->get('Content-Type') && - ( - 0 === \mb_strpos($request->headers->get('Content-Type'), 'application/json') || - 0 === \mb_strpos($request->headers->get('Content-Type'), 'application/ld+json') - ) - ) { - return true; - } - - if ( - in_array('application/json', $request->getAcceptableContentTypes()) || - in_array('application/ld+json', $request->getAcceptableContentTypes()) - ) { - return true; - } - - return false; - } - - /** - * @param string $string - * @param string|null $foreground_color - * @param string|null $background_color - * @return string - */ - public function getColoredString(string $string, $foreground_color = null, $background_color = null): string - { - $colored_string = ""; - - // Check if given foreground color found - if (isset($this->foreground_colors[$foreground_color])) { - $colored_string .= "\033[" . $this->foreground_colors[$foreground_color] . "m"; - } - // Check if given background color found - if (isset($this->background_colors[$background_color])) { - $colored_string .= "\033[" . $this->background_colors[$background_color] . "m"; - } - - // Add string and end coloring - $colored_string .= $string . "\033[0m"; - - return $colored_string; - } - - /** - * @return array Returns all foreground color names - */ - public function getForegroundColors(): array - { - return array_keys($this->foreground_colors); - } - - /** - * @return array Returns all background color names - */ - public function getBackgroundColors(): array - { - return array_keys($this->background_colors); - } -} diff --git a/src/Exception/TooManyLoginAttemptsException.php b/src/Exception/TooManyLoginAttemptsException.php deleted file mode 100644 index a15276b0..00000000 --- a/src/Exception/TooManyLoginAttemptsException.php +++ /dev/null @@ -1,18 +0,0 @@ - 'ASC'] ); foreach ($attributes as $attribute) { - if (null !== $attribute->getGroup()) { - if (!isset($choices[$attribute->getGroup()->getName()])) { - $choices[$attribute->getGroup()->getName()] = []; + $label = $attribute->getLabelOrCode($options['translation']); + if ( + null !== $attribute->getGroup() && + null !== $groupName = $attribute->getGroup()->getName() + ) { + if (!isset($choices[$groupName]) || !is_array($choices[$groupName])) { + $choices[$groupName] = []; } - $choices[$attribute->getGroup()->getName()][$attribute->getLabelOrCode($options['translation'])] = $attribute->getId(); + $choices[$groupName][$label] = $attribute->getId(); } else { - $choices[$attribute->getLabelOrCode($options['translation'])] = $attribute->getId(); + $choices[$label] = $attribute->getId(); } } return $choices; diff --git a/src/Form/AttributeType.php b/src/Form/AttributeType.php index f4400d74..cd461657 100644 --- a/src/Form/AttributeType.php +++ b/src/Form/AttributeType.php @@ -10,6 +10,7 @@ use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\CollectionType; +use Symfony\Component\Form\Extension\Core\Type\NumberType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -59,6 +60,18 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'required' => false, 'help' => 'attributes.form_help.searchable' ]) + ->add('weight', NumberType::class, [ + 'label' => 'attributes.weight', + 'required' => false, + 'scale' => 1, + 'help' => 'attributes.form_help.weight' + ]) + ->add('defaultRealm', RealmChoiceType::class, [ + 'label' => 'attributes.defaultRealm', + 'help' => 'attributes.defaultRealm.help', + 'placeholder' => 'attributes.defaultRealm.placeholder', + 'required' => false, + ]) ->add('attributeTranslations', CollectionType::class, [ 'label' => 'attributes.form.attributeTranslations', 'allow_add' => true, diff --git a/src/Form/AttributeValueRealmType.php b/src/Form/AttributeValueRealmType.php new file mode 100644 index 00000000..540fb051 --- /dev/null +++ b/src/Form/AttributeValueRealmType.php @@ -0,0 +1,30 @@ +add('realm', RealmChoiceType::class, [ + 'label' => false, + 'placeholder' => 'attributeValue.realm.placeholder', + 'required' => false, + ]); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'label' => false, + 'data_class' => AttributeValueInterface::class, + ]); + } +} diff --git a/src/Form/AttributeValueTranslationType.php b/src/Form/AttributeValueTranslationType.php index d6e5ad42..0e3b2e8b 100644 --- a/src/Form/AttributeValueTranslationType.php +++ b/src/Form/AttributeValueTranslationType.php @@ -13,10 +13,12 @@ use Symfony\Component\Form\Extension\Core\Type\DateTimeType; use Symfony\Component\Form\Extension\Core\Type\DateType; use Symfony\Component\Form\Extension\Core\Type\EmailType; +use Symfony\Component\Form\Extension\Core\Type\FormType; use Symfony\Component\Form\Extension\Core\Type\IntegerType; use Symfony\Component\Form\Extension\Core\Type\NumberType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Validator\Constraints\Email; use Symfony\Component\Validator\Constraints\Length; @@ -103,6 +105,9 @@ public function buildForm(FormBuilderInterface $builder, array $options): void break; } } + $builder->add('attributeValue', AttributeValueRealmType::class, [ + 'label' => false, + ]); } /** @@ -134,6 +139,15 @@ protected function getOptions(AttributeValueTranslationInterface $attributeValue ], $options ?: []); } + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'label' => false, + 'data_class' => AttributeValueTranslationInterface::class, + ]); + } + + /** * @inheritDoc */ diff --git a/src/Form/Constraint/NodeTypeFieldValidator.php b/src/Form/Constraint/NodeTypeFieldValidator.php index 9ba3b5c0..696252df 100644 --- a/src/Form/Constraint/NodeTypeFieldValidator.php +++ b/src/Form/Constraint/NodeTypeFieldValidator.php @@ -4,6 +4,7 @@ namespace RZ\Roadiz\CoreBundle\Form\Constraint; +use Doctrine\Persistence\ManagerRegistry; use RZ\Roadiz\Core\AbstractEntities\PersistableInterface; use RZ\Roadiz\CoreBundle\Configuration\CollectionFieldConfiguration; use RZ\Roadiz\CoreBundle\Configuration\JoinNodeTypeFieldConfiguration; @@ -19,23 +20,46 @@ class NodeTypeFieldValidator extends ConstraintValidator { + public function __construct( + private readonly ManagerRegistry $registry, + ) { + } + public function validate(mixed $value, Constraint $constraint): void { - if ($value instanceof NodeTypeFieldEntity) { - if ($value->isMarkdown()) { - $this->validateMarkdownOptions($value); - } - if ($value->isManyToMany() || $value->isManyToOne()) { - $this->validateJoinTypes($value, $constraint); - } - if ($value->isMultiProvider() || $value->isSingleProvider()) { - $this->validateProviderTypes($value, $constraint); + if (!$value instanceof NodeTypeFieldEntity) { + $this->context->buildViolation('Value is not a valid NodeTypeField.')->addViolation(); + return; + } + + $existingNodeTypeFieldsByName = $this->registry->getRepository(NodeTypeFieldEntity::class)->findBy([ + 'name' => $value->getName(), + ]); + foreach ($existingNodeTypeFieldsByName as $item) { + if ($item->getId() === $value->getId()) { + continue; } - if ($value->isCollection()) { - $this->validateCollectionTypes($value, $constraint); + if ($item->getDoctrineType() !== $value->getDoctrineType()) { + $this->context->buildViolation('field_with_same_name_already_exists_but_with_different_doctrine_type') + ->setParameter('%name%', $item->getName()) + ->setParameter('%nodeTypeName%', $item->getNodeTypeName()) + ->setParameter('%type%', $item->getDoctrineType()) + ->atPath('name') + ->addViolation(); } - } else { - $this->context->buildViolation('Value is not a valid NodeTypeField.')->addViolation(); + } + + if ($value->isMarkdown()) { + $this->validateMarkdownOptions($value); + } + if ($value->isManyToMany() || $value->isManyToOne()) { + $this->validateJoinTypes($value, $constraint); + } + if ($value->isMultiProvider() || $value->isSingleProvider()) { + $this->validateProviderTypes($value, $constraint); + } + if ($value->isCollection()) { + $this->validateCollectionTypes($value, $constraint); } } diff --git a/src/Form/Constraint/UniqueEntity.php b/src/Form/Constraint/UniqueEntity.php deleted file mode 100644 index f5bfd400..00000000 --- a/src/Form/Constraint/UniqueEntity.php +++ /dev/null @@ -1,42 +0,0 @@ - - * @see https://github.com/symfony/doctrine-bridge/blob/master/Validator/Constraints/UniqueEntity.php - * @deprecated Use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity - */ -class UniqueEntity extends Constraint -{ - public const NOT_UNIQUE_ERROR = '23bd9dbf-6b9b-41cd-a99e-4844bcf3077f'; - - public string $message = 'value.is.already.used'; - /** - * @var class-string|null - */ - public ?string $entityClass = null; - public string $repositoryMethod = 'findBy'; - public ?string $errorPath = null; - public array $fields = []; - public bool $ignoreNull = true; - - public function getRequiredOptions(): array - { - return ['fields']; - } - - public function getDefaultOption(): string - { - return 'fields'; - } -} diff --git a/src/Form/Constraint/UniqueEntityValidator.php b/src/Form/Constraint/UniqueEntityValidator.php deleted file mode 100644 index 701af696..00000000 --- a/src/Form/Constraint/UniqueEntityValidator.php +++ /dev/null @@ -1,171 +0,0 @@ - - * @package RZ\Roadiz\CoreBundle\Form\Constraint - * @see https://github.com/symfony/doctrine-bridge/blob/master/Validator/Constraints/UniqueEntityValidator.php - * @deprecated Use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntityValidator - */ -class UniqueEntityValidator extends ConstraintValidator -{ - protected ManagerRegistry $managerRegistry; - - /** - * @param ManagerRegistry $managerRegistry - */ - public function __construct(ManagerRegistry $managerRegistry) - { - $this->managerRegistry = $managerRegistry; - } - - /** - * @param mixed $value - * @param UniqueEntity $constraint - * - * @throws \Exception - */ - public function validate(mixed $value, Constraint $constraint): void - { - if (!$constraint instanceof UniqueEntity) { - throw new UnexpectedTypeException($constraint, __NAMESPACE__ . '\UniqueEntity'); - } - - $fields = $constraint->fields; - if (0 === count($fields)) { - throw new ConstraintDefinitionException('At least one field has to be specified.'); - } - - $class = $this->managerRegistry - ->getManagerForClass(get_class($value)) - ->getClassMetadata(get_class($value)); - - $criteria = []; - $hasNullValue = false; - foreach ($fields as $fieldName) { - if (!$class instanceof ClassMetadataInfo) { - throw new ConstraintDefinitionException(sprintf('The class "%s" is not mapped by Doctrine, so it cannot be validated for uniqueness.', get_class($value))); - } - if (!$class->hasField($fieldName) && !$class->hasAssociation($fieldName)) { - throw new ConstraintDefinitionException(sprintf('The field "%s" is not mapped by Doctrine, so it cannot be validated for uniqueness.', $fieldName)); - } - $fieldValue = $class->getReflectionProperty($fieldName)->getValue($value); - - if (null === $fieldValue) { - $hasNullValue = true; - } - if ($constraint->ignoreNull && null === $fieldValue) { - continue; - } - $criteria[$fieldName] = $fieldValue; - if (null !== $criteria[$fieldName] && $class->hasAssociation($fieldName)) { - /* Ensure the Proxy is initialized before using reflection to - * read its identifiers. This is necessary because the wrapped - * getter methods in the Proxy are being bypassed. - */ - $this->managerRegistry - ->getManagerForClass(get_class($value)) - ->initializeObject($criteria[$fieldName]); - } - } - // validation doesn't fail if one of the fields is null and if null values should be ignored - if ($hasNullValue && $constraint->ignoreNull) { - return; - } - // skip validation if there are no criteria (this can happen when the - // "ignoreNull" option is enabled and fields to be checked are null - if (empty($criteria)) { - return; - } - if (null !== $constraint->entityClass) { - /* Retrieve repository from given entity name. - * We ensure the retrieved repository can handle the entity - * by checking the entity is the same, or subclass of the supported entity. - */ - $repository = $this->managerRegistry->getRepository($constraint->entityClass); - $supportedClass = $repository->getClassName(); - if (!$value instanceof $supportedClass) { - throw new ConstraintDefinitionException(sprintf('The "%s" entity repository does not support the "%s" entity. The entity should be an instance of or extend "%s".', $constraint->entityClass, $class->getName(), $supportedClass)); - } - } else { - $repository = $this->managerRegistry->getRepository(get_class($value)); - } - $result = $repository->{$constraint->repositoryMethod}($criteria); - if ($result instanceof \IteratorAggregate) { - $result = $result->getIterator(); - } - /* If the result is a MongoCursor, it must be advanced to the first - * element. Rewinding should have no ill effect if $result is another - * iterator implementation. - */ - if ($result instanceof \Iterator) { - $result->rewind(); - } elseif (is_array($result)) { - reset($result); - } - /* If no entity matched the query criteria or a single entity matched, - * which is the same as the entity being validated, the criteria is - * unique. - */ - if (0 === count($result) || (1 === count($result) && $value === ($result instanceof \Iterator ? $result->current() : current($result)))) { - return; - } - - $errorPath = null !== $constraint->errorPath ? $constraint->errorPath : $fields[0]; - $invalidValue = $criteria[$errorPath] ?? $criteria[$fields[0]]; - - $this->context->buildViolation($constraint->message) - ->atPath($errorPath) - ->setParameter('{{ value }}', $this->formatWithIdentifiers($class, $invalidValue)) - ->setInvalidValue($invalidValue) - ->setCode(UniqueEntity::NOT_UNIQUE_ERROR) - ->addViolation(); - } - - private function formatWithIdentifiers(ClassMetadata $class, mixed $value): string - { - if (!is_object($value) || $value instanceof \DateTimeInterface) { - return $this->formatValue($value, self::PRETTY_DATE); - } - if ($class->getName() !== $idClass = get_class($value)) { - // non unique value might be a composite PK that consists of other entity objects - if ($this->managerRegistry->getManagerForClass($idClass)->getMetadataFactory()->hasMetadataFor($idClass)) { - $identifiers = $this->managerRegistry - ->getManagerForClass($idClass) - ->getClassMetadata($idClass) - ->getIdentifierValues($value); - } else { - // this case might happen if the non unique column has a custom doctrine type and its value is an object - // in which case we cannot get any identifiers for it - $identifiers = []; - } - } else { - $identifiers = $class->getIdentifierValues($value); - } - if (!$identifiers) { - return sprintf('object("%s")', $idClass); - } - array_walk($identifiers, function (&$id, $field) { - if (!is_object($id) || $id instanceof \DateTimeInterface) { - $idAsString = $this->formatValue($id, self::PRETTY_DATE); - } else { - $idAsString = sprintf('object("%s")', get_class($id)); - } - $id = sprintf('%s => %s', $field, $idAsString); - }); - return sprintf('object("%s") identified by (%s)', $idClass, implode(', ', $identifiers)); - } -} diff --git a/src/Form/CustomFormsType.php b/src/Form/CustomFormsType.php index ad40496f..5cd0711c 100644 --- a/src/Form/CustomFormsType.php +++ b/src/Form/CustomFormsType.php @@ -105,11 +105,12 @@ protected function getFieldsByGroups(array $options): array /** @var CustomFormField $field */ foreach ($fields as $field) { - if ($field->getGroupName() != '') { - if (!isset($fieldsArray[$field->getGroupName()])) { - $fieldsArray[$field->getGroupName()] = []; + $groupName = $field->getGroupName(); + if (\is_string($groupName) && $groupName !== '') { + if (!isset($fieldsArray[$groupName]) || !\is_array($fieldsArray[$groupName])) { + $fieldsArray[$groupName] = []; } - $fieldsArray[$field->getGroupName()][] = $field; + $fieldsArray[$groupName][] = $field; } else { $fieldsArray[] = $field; } diff --git a/src/Form/DataListTextType.php b/src/Form/DataListTextType.php new file mode 100644 index 00000000..4cf6b095 --- /dev/null +++ b/src/Form/DataListTextType.php @@ -0,0 +1,43 @@ +setRequired('listName'); + $resolver->setAllowedTypes('listName', 'string'); + $resolver->setRequired('list'); + $resolver->setAllowedTypes('list', 'array'); + } + + public function buildView(FormView $view, FormInterface $form, array $options): void + { + parent::buildView($view, $form, $options); + + $view->vars['listName'] = $options['listName']; + $view->vars['list'] = $options['list']; + } + + + public function getBlockPrefix(): string + { + return 'data_list_text'; + } + + public function getParent(): string + { + return TextType::class; + } +} diff --git a/src/Form/DataTransformer/ExplorerProviderItemTransformer.php b/src/Form/DataTransformer/ExplorerProviderItemTransformer.php index 6d899917..f94a7863 100644 --- a/src/Form/DataTransformer/ExplorerProviderItemTransformer.php +++ b/src/Form/DataTransformer/ExplorerProviderItemTransformer.php @@ -70,7 +70,7 @@ public function reverseTransform(mixed $value): mixed $items = []; } elseif ($value instanceof ExplorerItemInterface) { $items = [$value]; - } elseif (is_scalar($value)) { + } elseif (\is_string($value) || \is_int($value)) { $items = $this->explorerProvider->getItemsById([$value]); } elseif (\is_array($value) && is_scalar(reset($value))) { $items = $this->explorerProvider->getItemsById($value); diff --git a/src/Form/DataTransformer/JoinDataTransformer.php b/src/Form/DataTransformer/JoinDataTransformer.php index 72825f6b..5259300a 100644 --- a/src/Form/DataTransformer/JoinDataTransformer.php +++ b/src/Form/DataTransformer/JoinDataTransformer.php @@ -24,7 +24,7 @@ class JoinDataTransformer implements DataTransformerInterface /** * @param NodeTypeField $nodeTypeField * @param ManagerRegistry $managerRegistry - * @param string $entityClassname + * @param class-string $entityClassname */ public function __construct( NodeTypeField $nodeTypeField, @@ -74,6 +74,7 @@ public function transform(mixed $value): array public function reverseTransform(mixed $value): mixed { if ($this->nodeTypeField->isManyToMany()) { + /** @var PersistableInterface[] $unorderedEntities */ $unorderedEntities = $this->managerRegistry->getRepository($this->entityClassname)->findBy([ 'id' => $value, ]); diff --git a/src/Form/RoleEntityType.php b/src/Form/RoleEntityType.php index 6e0a4cf9..44d53e57 100644 --- a/src/Form/RoleEntityType.php +++ b/src/Form/RoleEntityType.php @@ -10,7 +10,7 @@ use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\Security\Core\Security; +use Symfony\Bundle\SecurityBundle\Security; final class RoleEntityType extends AbstractType { diff --git a/src/Form/SettingType.php b/src/Form/SettingType.php index cd5b2613..56392a67 100644 --- a/src/Form/SettingType.php +++ b/src/Form/SettingType.php @@ -48,10 +48,6 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'label' => 'visible', 'required' => false, ]) - ->add('encrypted', CheckboxType::class, [ - 'label' => 'encrypted', - 'required' => false, - ]) ->add('type', ChoiceType::class, [ 'label' => 'type', 'required' => true, diff --git a/src/Form/ThemesType.php b/src/Form/ThemesType.php index de4becbb..e47a6590 100644 --- a/src/Form/ThemesType.php +++ b/src/Form/ThemesType.php @@ -28,7 +28,9 @@ public function configureOptions(OptionsResolver $resolver): void $value = []; foreach ($options['themes_config'] as $themeConfig) { $class = $themeConfig['classname']; - $value[call_user_func([$class, 'getThemeName'])] = $class; + /** @var callable $callable */ + $callable = [$class, 'getThemeName']; + $value[call_user_func($callable)] = $class; } return $value; }); diff --git a/src/Importer/NodeTypesImporter.php b/src/Importer/NodeTypesImporter.php index da1830a7..0a8c4077 100644 --- a/src/Importer/NodeTypesImporter.php +++ b/src/Importer/NodeTypesImporter.php @@ -6,29 +6,16 @@ use JMS\Serializer\DeserializationContext; use JMS\Serializer\SerializerInterface; -use RZ\Roadiz\Core\Handlers\HandlerFactoryInterface; use RZ\Roadiz\CoreBundle\Entity\NodeType; -use RZ\Roadiz\CoreBundle\EntityHandler\NodeTypeHandler; -use RZ\Roadiz\CoreBundle\Message\UpdateNodeTypeSchemaMessage; use RZ\Roadiz\CoreBundle\Serializer\ObjectConstructor\TypedObjectConstructorInterface; -use Symfony\Component\Messenger\Envelope; -use Symfony\Component\Messenger\MessageBusInterface; class NodeTypesImporter implements EntityImporterInterface { - protected SerializerInterface $serializer; - protected HandlerFactoryInterface $handlerFactory; - - - public function __construct(SerializerInterface $serializer, HandlerFactoryInterface $handlerFactory) - { - $this->serializer = $serializer; - $this->handlerFactory = $handlerFactory; + public function __construct( + protected SerializerInterface $serializer + ) { } - /** - * @inheritDoc - */ public function supports(string $entityClass): bool { return $entityClass === NodeType::class; @@ -39,7 +26,7 @@ public function supports(string $entityClass): bool */ public function import(string $serializedData): bool { - $nodeType = $this->serializer->deserialize( + $this->serializer->deserialize( $serializedData, NodeType::class, 'json', @@ -48,10 +35,6 @@ public function import(string $serializedData): bool ->setAttribute(TypedObjectConstructorInterface::FLUSH_NEW_OBJECTS, true) ); - /** @var NodeTypeHandler $nodeTypeHandler */ - $nodeTypeHandler = $this->handlerFactory->getHandler($nodeType); - $nodeTypeHandler->updateSchema(); - return true; } } diff --git a/src/ListManager/AbstractEntityListManager.php b/src/ListManager/AbstractEntityListManager.php index 6e54c0e3..128c964c 100644 --- a/src/ListManager/AbstractEntityListManager.php +++ b/src/ListManager/AbstractEntityListManager.php @@ -13,6 +13,7 @@ abstract class AbstractEntityListManager implements EntityListManagerInterface protected ?array $queryArray = null; protected ?int $currentPage = null; protected ?int $itemPerPage = null; + protected ?string $searchPattern = null; protected bool $displayNotPublishedNodes; protected bool $displayAllNodesStatuses; protected bool $allowRequestSorting = true; @@ -136,6 +137,7 @@ public function getAssignation(): array 'pageCount' => $this->getPageCount(), 'itemPerPage' => $this->getItemPerPage(), 'itemCount' => $this->getItemCount(), + 'search' => $this->searchPattern, 'nextPageQuery' => null, 'previousPageQuery' => null, ]; @@ -214,6 +216,72 @@ public function getPageCount(): int return (int) ceil($this->getItemCount() / $this->getItemPerPage()); } + protected function handleRequestQuery(bool $disabled): void + { + if ($disabled || null === $this->request) { + /* + * Disable pagination and paginator + */ + $this->disablePagination(); + return; + } + + $field = $this->request->query->get('field'); + $ordering = $this->request->query->get('ordering'); + $search = $this->request->query->get('search'); + $itemPerPage = $this->request->query->get('item_per_page') ?? + $this->request->query->get('itemPerPage') ?? + $this->request->query->get('itemsPerPage'); + $page = $this->request->query->get('page'); + + if ( + $this->allowRequestSorting && + \is_string($field) && + $field !== "" && + \is_string($ordering) && + \in_array(strtolower($ordering), ['asc', 'desc']) + ) { + $this->handleOrderingParam($field, $ordering); + $this->queryArray['field'] = $field; + $this->queryArray['ordering'] = $ordering; + } + + if ( + $this->allowRequestSearching && + \is_string($search) && + $search !== "" + ) { + $this->handleSearchParam($search); + $this->queryArray['search'] = $search; + } + + if ( + \is_numeric($itemPerPage) && + ((int) $itemPerPage) > 0 + ) { + $this->setItemPerPage((int) $itemPerPage); + } + + if ( + \is_numeric($page) && + ((int) $page) > 1 + ) { + $this->setPage((int) $page); + } else { + $this->setPage(1); + } + } + + protected function handleSearchParam(string $search): void + { + $this->searchPattern = $search; + } + + protected function handleOrderingParam(string $field, string $ordering): void + { + // Do nothing on abstract + } + protected function validateOrderingFieldName(string $field): void { // check if field is a valid name without any SQL injection diff --git a/src/ListManager/EntityListManager.php b/src/ListManager/EntityListManager.php index 7822db82..f46adaa8 100644 --- a/src/ListManager/EntityListManager.php +++ b/src/ListManager/EntityListManager.php @@ -4,6 +4,7 @@ namespace RZ\Roadiz\CoreBundle\ListManager; +use Doctrine\ORM\Tools\Pagination\Paginator as DoctrinePaginator; use Doctrine\Persistence\ObjectManager; use RZ\Roadiz\Core\AbstractEntities\PersistableInterface; use RZ\Roadiz\Core\AbstractEntities\TranslationInterface; @@ -13,7 +14,6 @@ use RZ\Roadiz\CoreBundle\Repository\NodeRepository; use RZ\Roadiz\CoreBundle\Repository\StatusAwareRepository; use Symfony\Component\HttpFoundation\Request; -use Doctrine\ORM\Tools\Pagination\Paginator as DoctrinePaginator; /** * Perform basic filtering and search over entity listings. @@ -28,7 +28,6 @@ class EntityListManager extends AbstractEntityListManager protected ?Paginator $paginator = null; protected ?array $orderingArray = null; protected ?array $filteringArray = null; - protected ?string $searchPattern = null; protected ?array $assignation = null; protected ?TranslationInterface $translation = null; @@ -119,59 +118,30 @@ public function handle(bool $disabled = false) unset($this->filteringArray["chroot"]); // remove placeholder } - if (false === $disabled && null !== $this->request) { - if ( - $this->allowRequestSorting && - $this->request->query->get('field') && - $this->request->query->get('ordering') - ) { - $this->validateOrderingFieldName($this->request->query->get('field')); - $this->orderingArray = [ - $this->request->query->get('field') => $this->request->query->get('ordering') - ]; - $this->queryArray['field'] = $this->request->query->get('field'); - $this->queryArray['ordering'] = $this->request->query->get('ordering'); - } - - if ($this->allowRequestSearching && $this->request->query->get('search') != "") { - $this->searchPattern = $this->request->query->get('search'); - $this->queryArray['search'] = $this->request->query->get('search'); - } - - if ( - $this->request->query->has('item_per_page') && - $this->request->query->get('item_per_page') > 0 - ) { - $this->setItemPerPage((int) $this->request->query->get('item_per_page')); - } - - if ( - $this->request->query->has('page') && - $this->request->query->get('page') > 1 - ) { - $this->setPage((int) $this->request->query->get('page')); - } else { - $this->setPage(1); - } - } else { - /* - * Disable pagination and paginator - */ - $this->disablePagination(); - } - + $this->handleRequestQuery($disabled); $this->createPaginator(); if ( $this->allowRequestSearching && false === $disabled && - null !== $this->request && - $this->request->query->get('search') != "" + null !== $this->request ) { - $this->paginator->setSearchPattern($this->request->query->get('search')); + $search = $this->request->query->get('search'); + if (\is_string($search) && $search !== "") { + $this->paginator->setSearchPattern($search); + } } } + protected function handleOrderingParam(string $field, string $ordering): void + { + $this->validateOrderingFieldName($field); + $this->orderingArray = [ + $field => $ordering + ]; + } + + protected function createPaginator(): void { if ( @@ -213,16 +183,6 @@ protected function createPaginator(): void $this->paginator->setDisplayingAllNodesStatuses($this->isDisplayingAllNodesStatuses()); } - /** - * @return array - */ - public function getAssignation(): array - { - return array_merge(parent::getAssignation(), [ - 'search' => $this->searchPattern, - ]); - } - /** * @return int */ diff --git a/src/ListManager/NodePaginator.php b/src/ListManager/NodePaginator.php index 0b3816d3..3f8813fa 100644 --- a/src/ListManager/NodePaginator.php +++ b/src/ListManager/NodePaginator.php @@ -39,11 +39,11 @@ public function setTranslation(TranslationInterface $translation = null) * Return entities filtered for current page. * * @param array $order - * @param integer $page + * @param int $page * - * @return array + * @return array|\Doctrine\ORM\Tools\Pagination\Paginator */ - public function findByAtPage(array $order = [], $page = 1) + public function findByAtPage(array $order = [], int $page = 1) { if (null !== $this->searchPattern) { return $this->searchByAtPage($order, $page); diff --git a/src/ListManager/NodesSourcesPaginator.php b/src/ListManager/NodesSourcesPaginator.php index 288d0611..c6c746ff 100644 --- a/src/ListManager/NodesSourcesPaginator.php +++ b/src/ListManager/NodesSourcesPaginator.php @@ -33,7 +33,7 @@ public function getTotalCount(): int * @param array $order * @param integer $page * - * @return array + * @return array|\Doctrine\ORM\Tools\Pagination\Paginator */ public function findByAtPage(array $order = [], int $page = 1) { diff --git a/src/ListManager/QueryBuilderListManager.php b/src/ListManager/QueryBuilderListManager.php index d220f474..98f94230 100644 --- a/src/ListManager/QueryBuilderListManager.php +++ b/src/ListManager/QueryBuilderListManager.php @@ -14,6 +14,10 @@ class QueryBuilderListManager extends AbstractEntityListManager protected ?Paginator $paginator = null; protected string $identifier; protected bool $debug = false; + /** + * @var null|callable + */ + protected $searchingCallable = null; /** * @param Request|null $request @@ -33,57 +37,40 @@ public function __construct( $this->debug = $debug; } + /** + * @param callable|null $searchingCallable + * @return QueryBuilderListManager + */ + public function setSearchingCallable(?callable $searchingCallable): QueryBuilderListManager + { + $this->searchingCallable = $searchingCallable; + return $this; + } + /** * @param string $search */ protected function handleSearchParam(string $search): void { - // Implement your custom logic + parent::handleSearchParam($search); + + if (\is_callable($this->searchingCallable)) { + \call_user_func($this->searchingCallable, $this->queryBuilder, $search); + } } public function handle(bool $disabled = false) { - if (false === $disabled && null !== $this->request) { - if ( - $this->allowRequestSorting && - $this->request->query->get('field') && - $this->request->query->get('ordering') - ) { - $this->validateOrderingFieldName($this->request->query->get('field')); - $this->queryBuilder->addOrderBy( - sprintf('%s.%s', $this->identifier, $this->request->query->get('field')), - $this->request->query->get('ordering') - ); - $this->queryArray['field'] = $this->request->query->get('field'); - $this->queryArray['ordering'] = $this->request->query->get('ordering'); - } - - if ($this->allowRequestSearching && $this->request->query->get('search') != "") { - $this->handleSearchParam($this->request->query->get('search')); - $this->queryArray['search'] = $this->request->query->get('search'); - } - - if ( - $this->request->query->has('item_per_page') && - $this->request->query->get('item_per_page') > 0 - ) { - $this->setItemPerPage((int) $this->request->query->get('item_per_page')); - } - - if ( - $this->request->query->has('page') && - $this->request->query->get('page') > 1 - ) { - $this->setPage((int) $this->request->query->get('page')); - } else { - $this->setPage(1); - } - } else { - /* - * Disable pagination and paginator - */ - $this->disablePagination(); - } + $this->handleRequestQuery($disabled); + } + + protected function handleOrderingParam(string $field, string $ordering): void + { + $this->validateOrderingFieldName($field); + $this->queryBuilder->addOrderBy( + sprintf('%s.%s', $this->identifier, $field), + $ordering + ); } /** diff --git a/src/ListManager/SessionListFilters.php b/src/ListManager/SessionListFilters.php new file mode 100644 index 00000000..4e8dc5f7 --- /dev/null +++ b/src/ListManager/SessionListFilters.php @@ -0,0 +1,53 @@ +hasSession() && + $request->getSession()->has($this->sessionIdentifier) && + $request->getSession()->get($this->sessionIdentifier) > 0 && + (!$request->query->has('item_per_page') || + $request->query->get('item_per_page') < 1) + ) { + /* + * Item count is in session + */ + $request->query->set('item_per_page', intval($request->getSession()->get($this->sessionIdentifier))); + $listManager->setItemPerPage(intval($request->getSession()->get($this->sessionIdentifier))); + } elseif ( + $request->query->has('item_per_page') && + $request->query->get('item_per_page') > 0 + ) { + /* + * Item count is in query, save it in session + */ + $request->getSession()->set($this->sessionIdentifier, intval($request->query->get('item_per_page'))); + $listManager->setItemPerPage(intval($request->query->get('item_per_page'))); + } else { + $listManager->setItemPerPage($this->defaultItemsParPage); + } + } +} diff --git a/src/Logger/DoctrineHandler.php b/src/Logger/DoctrineHandler.php index 330dea02..bcc9e42c 100644 --- a/src/Logger/DoctrineHandler.php +++ b/src/Logger/DoctrineHandler.php @@ -7,9 +7,14 @@ use Doctrine\Persistence\ManagerRegistry; use Monolog\Handler\AbstractProcessingHandler; use Monolog\Logger; -use RZ\Roadiz\CoreBundle\Entity\Log; +use RZ\Roadiz\Core\AbstractEntities\PersistableInterface; +use RZ\Roadiz\CoreBundle\Entity\Node; use RZ\Roadiz\CoreBundle\Entity\NodesSources; use RZ\Roadiz\CoreBundle\Entity\User; +use RZ\Roadiz\CoreBundle\Logger\Entity\Log; +use RZ\Roadiz\Documents\Models\DocumentInterface; +use RZ\Roadiz\Documents\UrlGenerators\DocumentUrlGeneratorInterface; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Security\Core\User\UserInterface; @@ -19,40 +24,98 @@ */ final class DoctrineHandler extends AbstractProcessingHandler { - protected ManagerRegistry $managerRegistry; - protected TokenStorageInterface $tokenStorage; - protected RequestStack $requestStack; + private ManagerRegistry $managerRegistry; + private TokenStorageInterface $tokenStorage; + private RequestStack $requestStack; + private DocumentUrlGeneratorInterface $documentUrlGenerator; public function __construct( ManagerRegistry $managerRegistry, TokenStorageInterface $tokenStorage, RequestStack $requestStack, + DocumentUrlGeneratorInterface $documentUrlGenerator, $level = Logger::INFO, $bubble = true ) { + parent::__construct($level, $bubble); $this->tokenStorage = $tokenStorage; $this->requestStack = $requestStack; $this->managerRegistry = $managerRegistry; + $this->documentUrlGenerator = $documentUrlGenerator; + } - parent::__construct($level, $bubble); + protected function getThumbnailSourcePath(?DocumentInterface $thumbnail): ?string + { + if (null === $thumbnail || $thumbnail->isPrivate()) { + return null; + } + return $this->documentUrlGenerator + ->setDocument($thumbnail) + ->setOptions([ + "fit" => "150x150", + "quality" => 70, + ]) + ->getUrl(); } - /** - * @return TokenStorageInterface - */ - public function getTokenStorage(): TokenStorageInterface + protected function populateForNode(Node $value, Log $log, array &$data): void { - return $this->tokenStorage; + $log->setEntityClass(Node::class); + $log->setEntityId($value->getId()); + $data = array_merge( + $data, + [ + 'node_id' => $value->getId(), + 'entity_title' => $value->getNodeName(), + ] + ); + $nodeSource = $value->getNodeSources()->first() ?: null; + if (null !== $nodeSource) { + $data = array_merge( + $data, + [ + 'node_source_id' => $nodeSource->getId(), + 'translation_id' => $nodeSource->getTranslation()->getId(), + 'entity_title' => $nodeSource->getTitle() ?? $value->getNodeName(), + ] + ); + } + + $thumbnailSrc = $this->getThumbnailSourcePath($nodeSource?->getOneDisplayableDocument()); + if (null !== $thumbnailSrc) { + $data = array_merge( + $data, + [ + 'entity_thumbnail_src' => $thumbnailSrc, + ] + ); + } } - /** - * @param TokenStorageInterface $tokenStorage - * - * @return $this - */ - public function setTokenStorage(TokenStorageInterface $tokenStorage): DoctrineHandler + + protected function populateForNodesSources(NodesSources $value, Log $log, array &$data): void { - $this->tokenStorage = $tokenStorage; - return $this; + $log->setEntityClass(NodesSources::class); + $log->setEntityId($value->getId()); + $data = array_merge( + $data, + [ + 'node_source_id' => $value->getId(), + 'node_id' => $value->getNode()->getId(), + 'translation_id' => $value->getTranslation()->getId(), + 'entity_title' => $value->getTitle(), + ] + ); + + $thumbnail = $value->getOneDisplayableDocument(); + $thumbnailSrc = $this->getThumbnailSourcePath($thumbnail); + if (null !== $thumbnailSrc) { + $data = array_merge( + $data, + [ + 'entity_thumbnail_src' => $thumbnailSrc, + ] + ); + } } /** @@ -73,44 +136,89 @@ public function write(array $record): void $log->setChannel((string) $record['channel']); $data = $record['extra']; - if (isset($record['context']['exception']) && $record['context']['exception'] instanceof \Exception) { - $data = array_merge( - $data, - [ - get_class($record['context']['exception']) => $record['context']['exception']->getMessage() - ] - ); - } - if (isset($record['context']['request'])) { - $data = array_merge( - $data, - $record['context']['request'] - ); - } - if (isset($record['context']['username'])) { - $data = array_merge( - $data, - ['username' => $record['context']['username']] - ); + $context = $record['context']; + + if (\is_array($context)) { + foreach ($context as $key => $value) { + if ($value instanceof Node) { + $this->populateForNode($value, $log, $data); + } elseif ($value instanceof NodesSources) { + $this->populateForNodesSources($value, $log, $data); + } elseif ($key === 'entity' && $value instanceof PersistableInterface) { + $log->setEntityClass(get_class($value)); + $log->setEntityId($value->getId()); + + $texteable = ['getTitle', 'getName', '__toString']; + foreach ($texteable as $method) { + if (method_exists($value, $method)) { + $data = array_merge( + $data, + [ + 'entity_title' => $value->{$method}() + ] + ); + break; + } + } + } + if ($value instanceof \Exception) { + $data = array_merge( + $data, + [ + 'exception_class' => get_class($value), + 'message' => $value->getMessage() + ] + ); + } + if ($value instanceof Request) { + $data = array_merge( + $data, + [ + 'uri' => $value->getUri(), + 'schemeHost' => $value->getSchemeAndHttpHost(), + ] + ); + } + if ($key === 'request' && \is_array($value)) { + $data = array_merge( + $data, + $value + ); + } + if (\is_string($value) && !empty($value) && !\is_numeric($key)) { + $data = array_merge( + $data, + [$key => $value] + ); + } + if (\is_string($value) && !empty($value) && \in_array($key, ['user', 'username'])) { + $log->setUsername($value); + } + } } - $log->setAdditionalData($data); /* * Use available securityAuthorizationChecker to provide a valid user */ - if ( - null !== $this->getTokenStorage() && - null !== $token = $this->getTokenStorage()->getToken() - ) { + if (null !== $token = $this->tokenStorage->getToken()) { $user = $token->getUser(); if ($user instanceof UserInterface) { if ($user instanceof User) { $log->setUser($user); + $data = array_merge( + $data, + [ + 'user_email' => $user->getEmail(), + 'user_public_name' => $user->getPublicName(), + 'user_picture_url' => $user->getPictureUrl(), + 'user_id' => $user->getId() + ] + ); } else { - $log->setUsername($user->getUsername()); + $log->setUsername($user->getUserIdentifier()); } } else { - $log->setUsername($token->getUsername()); + $log->setUsername($token->getUserIdentifier()); } } @@ -121,15 +229,7 @@ public function write(array $record): void $log->setClientIp($this->requestStack->getMainRequest()->getClientIp()); } - /* - * Add a related node-source entity - */ - if ( - isset($record['context']['source']) && - $record['context']['source'] instanceof NodesSources - ) { - $log->setNodeSource($record['context']['source']); - } + $log->setAdditionalData($data); $manager->persist($log); $manager->flush(); diff --git a/src/Entity/Log.php b/src/Logger/Entity/Log.php similarity index 60% rename from src/Entity/Log.php rename to src/Logger/Entity/Log.php index c42579f1..8dbdf02f 100644 --- a/src/Entity/Log.php +++ b/src/Logger/Entity/Log.php @@ -2,20 +2,26 @@ declare(strict_types=1); -namespace RZ\Roadiz\CoreBundle\Entity; +namespace RZ\Roadiz\CoreBundle\Logger\Entity; use Doctrine\ORM\Mapping as ORM; +use JMS\Serializer\Annotation as Serializer; use Monolog\Logger; use RZ\Roadiz\Core\AbstractEntities\AbstractEntity; -use JMS\Serializer\Annotation as Serializer; +use RZ\Roadiz\CoreBundle\Entity\NodesSources; +use RZ\Roadiz\CoreBundle\Entity\User; use RZ\Roadiz\CoreBundle\Repository\LogRepository; use Symfony\Component\Serializer\Annotation as SymfonySerializer; +use Symfony\Component\Validator\Constraints as Assert; #[ ORM\Entity(repositoryClass: LogRepository::class), ORM\Table(name: "log"), ORM\Index(columns: ["datetime"]), - ORM\Index(columns: ["node_source_id", "datetime"], name: "log_ns_datetime"), + ORM\Index(columns: ["entity_class"]), + ORM\Index(columns: ["entity_class", "entity_id"]), + ORM\Index(columns: ["entity_class", "datetime"], name: "log_entity_class_datetime"), + ORM\Index(columns: ["entity_class", "entity_id", "datetime"], name: "log_entity_class_id_datetime"), ORM\Index(columns: ["username", "datetime"], name: "log_username_datetime"), ORM\Index(columns: ["user_id", "datetime"], name: "log_user_datetime"), ORM\Index(columns: ["level", "datetime"], name: "log_level_datetime"), @@ -27,25 +33,16 @@ ] class Log extends AbstractEntity { - public const EMERGENCY = Logger::EMERGENCY; - public const CRITICAL = Logger::CRITICAL; - public const ALERT = Logger::ALERT; - public const ERROR = Logger::ERROR; - public const WARNING = Logger::WARNING; - public const NOTICE = Logger::NOTICE; - public const INFO = Logger::INFO; - public const DEBUG = Logger::DEBUG; - public const LOG = Logger::INFO; - - #[ORM\ManyToOne(targetEntity: User::class)] - #[ORM\JoinColumn(name: 'user_id', referencedColumnName: 'id', unique: false, onDelete: 'SET NULL')] + #[ORM\Column(name: 'user_id', type: 'string', length: 36, unique: false, nullable: true)] #[SymfonySerializer\Groups(['log_user'])] #[Serializer\Groups(['log_user'])] - protected ?User $user = null; + // @phpstan-ignore-next-line + protected int|string|null $userId = null; - #[ORM\Column(name: 'username', type: 'string', nullable: true)] + #[ORM\Column(name: 'username', type: 'string', length: 255, nullable: true)] #[SymfonySerializer\Groups(['log_user'])] #[Serializer\Groups(['log_user'])] + #[Assert\Length(max: 255)] protected ?string $username = null; #[ORM\Column(name: 'message', type: 'text')] @@ -56,29 +53,45 @@ class Log extends AbstractEntity #[ORM\Column(name: 'level', type: 'integer', nullable: false)] #[SymfonySerializer\Groups(['log'])] #[Serializer\Groups(['log'])] - protected int $level = Log::DEBUG; + protected int $level = Logger::DEBUG; #[ORM\Column(name: 'datetime', type: 'datetime', nullable: false)] #[SymfonySerializer\Groups(['log'])] #[Serializer\Groups(['log'])] protected \DateTime $datetime; - #[ORM\ManyToOne(targetEntity: NodesSources::class, inversedBy: 'logs')] - #[ORM\JoinColumn(name: 'node_source_id', referencedColumnName: 'id', onDelete: 'SET NULL')] - #[SymfonySerializer\Groups(['log_sources'])] - #[Serializer\Groups(['log_sources'])] - protected ?NodesSources $nodeSource = null; - - #[ORM\Column(name: 'client_ip', type: 'string', unique: false, nullable: true)] + #[ORM\Column(name: 'client_ip', type: 'string', length: 46, unique: false, nullable: true)] #[SymfonySerializer\Groups(['log'])] #[Serializer\Groups(['log'])] + #[Assert\Length(max: 46)] protected ?string $clientIp = null; - #[ORM\Column(name: 'channel', type: 'string', unique: false, nullable: true)] + #[ORM\Column(name: 'channel', type: 'string', length: 64, unique: false, nullable: true)] #[SymfonySerializer\Groups(['log'])] #[Serializer\Groups(['log'])] + #[Assert\Length(max: 64)] protected ?string $channel = null; + /** + * @var class-string|null + */ + #[ORM\Column(name: 'entity_class', type: 'string', length: 255, unique: false, nullable: true)] + #[SymfonySerializer\Groups(['log'])] + #[Serializer\Groups(['log'])] + #[Assert\Length(max: 255)] + // @phpstan-ignore-next-line + protected ?string $entityClass = null; + + /** + * @var string|int|null + */ + #[ORM\Column(name: 'entity_id', type: 'string', length: 36, unique: false, nullable: true)] + #[SymfonySerializer\Groups(['log'])] + #[Serializer\Groups(['log'])] + #[Assert\Length(max: 36)] + // @phpstan-ignore-next-line + protected string|int|null $entityId = null; + #[ORM\Column(name: 'additional_data', type: 'json', unique: false, nullable: true)] #[SymfonySerializer\Groups(['log'])] #[Serializer\Groups(['log'])] @@ -97,9 +110,22 @@ public function __construct(int $level, string $message) $this->datetime = new \DateTime("now"); } - public function getUser(): ?User + /** + * @return int|string|null + */ + public function getUserId(): int|string|null { - return $this->user; + return $this->userId; + } + + /** + * @param int|string|null $userId + * @return Log + */ + public function setUserId(int|string|null $userId): Log + { + $this->userId = $userId; + return $this; } /** @@ -109,7 +135,7 @@ public function getUser(): ?User */ public function setUser(User $user): Log { - $this->user = $user; + $this->userId = $user->getId(); $this->username = $user->getUsername(); return $this; } @@ -159,27 +185,22 @@ public function getDatetime(): \DateTime } /** - * Get log related node-source. + * BC setter. * - * @return NodesSources|null - */ - public function getNodeSource(): ?NodesSources - { - return $this->nodeSource; - } - - /** * @param NodesSources|null $nodeSource * @return $this */ public function setNodeSource(?NodesSources $nodeSource): Log { - $this->nodeSource = $nodeSource; + if (null !== $nodeSource) { + $this->entityClass = NodesSources::class; + $this->entityId = $nodeSource->getId(); + } return $this; } /** - * @return string + * @return string|null */ public function getClientIp(): ?string { @@ -187,7 +208,7 @@ public function getClientIp(): ?string } /** - * @param string $clientIp + * @param string|null $clientIp * @return Log */ public function setClientIp(?string $clientIp): Log @@ -236,6 +257,42 @@ public function setChannel(?string $channel): Log return $this; } + /** + * @return class-string|null + */ + public function getEntityClass(): ?string + { + return $this->entityClass; + } + + /** + * @param class-string|null $entityClass + * @return Log + */ + public function setEntityClass(?string $entityClass): Log + { + $this->entityClass = $entityClass; + return $this; + } + + /** + * @return int|string|null + */ + public function getEntityId(): int|string|null + { + return $this->entityId; + } + + /** + * @param int|string|null $entityId + * @return Log + */ + public function setEntityId(int|string|null $entityId): Log + { + $this->entityId = $entityId; + return $this; + } + #[ORM\PrePersist] public function prePersist(): void { diff --git a/src/Mailer/ContactFormManager.php b/src/Mailer/ContactFormManager.php index 1c340f40..8fa3df41 100644 --- a/src/Mailer/ContactFormManager.php +++ b/src/Mailer/ContactFormManager.php @@ -56,10 +56,6 @@ class ContactFormManager extends EmailManager 'image/gif', ]; protected int $maxFileSize = 5242880; // 5MB - protected FormFactoryInterface $formFactory; - protected FormErrorSerializerInterface $formErrorSerializer; - protected ?string $recaptchaPrivateKey; - protected ?string $recaptchaPublicKey; /* * DO NOT DIRECTLY USE THIS CONSTRUCTOR @@ -67,20 +63,18 @@ class ContactFormManager extends EmailManager */ public function __construct( RequestStack $requestStack, - FormFactoryInterface $formFactory, TranslatorInterface $translator, Environment $templating, MailerInterface $mailer, Settings $settingsBag, DocumentUrlGeneratorInterface $documentUrlGenerator, - FormErrorSerializerInterface $formErrorSerializer, - ?string $recaptchaPrivateKey, - ?string $recaptchaPublicKey + protected readonly FormFactoryInterface $formFactory, + protected readonly FormErrorSerializerInterface $formErrorSerializer, + protected readonly ?string $recaptchaPrivateKey, + protected readonly ?string $recaptchaPublicKey ) { parent::__construct($requestStack, $translator, $templating, $mailer, $settingsBag, $documentUrlGenerator); - $this->formFactory = $formFactory; - $this->formErrorSerializer = $formErrorSerializer; $this->options = [ 'attr' => [ 'id' => 'contactForm', @@ -101,8 +95,6 @@ public function __construct( 'new.contact.form.%site%', ['%site%' => $this->settingsBag->get('site_name')] )); - $this->recaptchaPrivateKey = $recaptchaPrivateKey; - $this->recaptchaPublicKey = $recaptchaPublicKey; } /** @@ -130,7 +122,7 @@ public function setFormName(string $formName): ContactFormManager * * @return $this */ - public function disableCsrfProtection() + public function disableCsrfProtection(): self { $this->options['csrf_protection'] = false; return $this; @@ -153,7 +145,7 @@ public function getForm(): FormInterface * @see https://symfony.com/doc/4.4/reference/constraints/Email.html#strict * @return $this */ - public function setEmailStrictMode(bool $emailStrictMode = true) + public function setEmailStrictMode(bool $emailStrictMode = true): self { $this->emailStrictMode = $emailStrictMode; @@ -171,9 +163,9 @@ public function isEmailStrictMode(): bool * Adds email, name and message fields with their constraints. * * @param bool $useHoneypot - * @return ContactFormManager $this + * @return $this */ - public function withDefaultFields(bool $useHoneypot = true) + public function withDefaultFields(bool $useHoneypot = true): self { $this->getFormBuilder()->add('email', EmailType::class, [ 'label' => 'your.email', @@ -184,7 +176,7 @@ public function withDefaultFields(bool $useHoneypot = true) 'message' => 'email.not.valid', 'mode' => $this->isEmailStrictMode() ? Email::VALIDATION_MODE_STRICT : - Email::VALIDATION_MODE_LOOSE + Email::VALIDATION_MODE_HTML5 ]), ], ]) @@ -217,7 +209,7 @@ public function withDefaultFields(bool $useHoneypot = true) * @param string $honeypotName * @return $this */ - public function withHoneypot(string $honeypotName = 'eml') + public function withHoneypot(string $honeypotName = 'eml'): self { $this->getFormBuilder()->add($honeypotName, HoneypotType::class); return $this; @@ -229,7 +221,7 @@ public function withHoneypot(string $honeypotName = 'eml') * @param string $consentDescription * @return $this */ - public function withUserConsent(string $consentDescription = 'contact_form.user_consent') + public function withUserConsent(string $consentDescription = 'contact_form.user_consent'): self { $this->getFormBuilder()->add('consent', CheckboxType::class, [ 'label' => $consentDescription, @@ -276,7 +268,7 @@ public function getFormBuilder(): FormBuilderInterface public function withGoogleRecaptcha( string $name = 'recaptcha', string $validatorFieldName = Recaptcha::FORM_NAME - ) { + ): self { if ( !empty($this->recaptchaPublicKey) && !empty($this->recaptchaPrivateKey) @@ -634,7 +626,7 @@ public function getRedirectUrl(): ?string * * @return self */ - public function setRedirectUrl(?string $redirectUrl) + public function setRedirectUrl(?string $redirectUrl): self { $this->redirectUrl = $redirectUrl; @@ -646,7 +638,7 @@ public function setRedirectUrl(?string $redirectUrl) * * @return int */ - public function getMaxFileSize() + public function getMaxFileSize(): int { return $this->maxFileSize; } @@ -658,7 +650,7 @@ public function getMaxFileSize() * * @return self */ - public function setMaxFileSize($maxFileSize) + public function setMaxFileSize($maxFileSize): self { $this->maxFileSize = (int) $maxFileSize; @@ -670,7 +662,7 @@ public function setMaxFileSize($maxFileSize) * * @return array */ - public function getAllowedMimeTypes() + public function getAllowedMimeTypes(): array { return $this->allowedMimeTypes; } @@ -682,7 +674,7 @@ public function getAllowedMimeTypes() * * @return self */ - public function setAllowedMimeTypes(array $allowedMimeTypes) + public function setAllowedMimeTypes(array $allowedMimeTypes): self { $this->allowedMimeTypes = $allowedMimeTypes; @@ -692,7 +684,7 @@ public function setAllowedMimeTypes(array $allowedMimeTypes) /** * @return array */ - public function getOptions() + public function getOptions(): array { return $this->options; } @@ -700,9 +692,9 @@ public function getOptions() /** * @param array $options * - * @return ContactFormManager + * @return $this */ - public function setOptions($options) + public function setOptions(array $options): self { $this->options = $options; diff --git a/src/Mailer/EmailManager.php b/src/Mailer/EmailManager.php index 346ebf60..73890767 100644 --- a/src/Mailer/EmailManager.php +++ b/src/Mailer/EmailManager.php @@ -33,43 +33,30 @@ class EmailManager protected ?Address $origin = null; protected string $successMessage = 'email.successfully.sent'; protected string $failMessage = 'email.has.errors'; - protected TranslatorInterface $translator; - protected Environment $templating; - protected MailerInterface $mailer; protected ?string $emailTemplate = null; protected ?string $emailPlainTextTemplate = null; protected string $emailStylesheet; - protected RequestStack $requestStack; protected array $assignation; protected ?Email $message; - protected ?Settings $settingsBag; - protected ?DocumentUrlGeneratorInterface $documentUrlGenerator; /** @var File[] */ protected array $files = []; /** @var array */ protected array $resources = []; public function __construct( - RequestStack $requestStack, - TranslatorInterface $translator, - Environment $templating, - MailerInterface $mailer, - ?Settings $settingsBag = null, - ?DocumentUrlGeneratorInterface $documentUrlGenerator = null + protected readonly RequestStack $requestStack, + protected readonly TranslatorInterface $translator, + protected readonly Environment $templating, + protected readonly MailerInterface $mailer, + protected readonly Settings $settingsBag, + protected readonly DocumentUrlGeneratorInterface $documentUrlGenerator ) { - $this->requestStack = $requestStack; - $this->translator = $translator; - $this->mailer = $mailer; - $this->templating = $templating; $this->assignation = []; $this->message = null; - /* * Sets a default CSS for emails. */ $this->emailStylesheet = dirname(__DIR__) . '/../css/transactionalStyles.css'; - $this->settingsBag = $settingsBag; - $this->documentUrlGenerator = $documentUrlGenerator; } /** @@ -93,9 +80,13 @@ public function renderHtmlEmailBodyWithCss(): string { if (null !== $this->getEmailStylesheet()) { $htmldoc = new InlineStyle($this->renderHtmlEmailBody()); - $htmldoc->applyStylesheet(file_get_contents( + $css = file_get_contents( $this->getEmailStylesheet() - )); + ); + if (false === $css) { + throw new \RuntimeException('Unable to read email stylesheet file.'); + } + $htmldoc->applyStylesheet($css); return $htmldoc->getHTML(); } @@ -274,7 +265,7 @@ public function getReceiverEmail(): ?string * @return $this * @throws \Exception */ - public function setReceiver($receiver): static + public function setReceiver(mixed $receiver): static { if ($receiver instanceof Address) { $this->receiver = [$receiver]; @@ -329,7 +320,7 @@ public function getSenderEmail(): ?string * @return $this * @throws \Exception */ - public function setSender($sender): static + public function setSender(mixed $sender): static { if ($sender instanceof Address) { $this->sender = [$sender]; @@ -397,16 +388,6 @@ public function getTranslator(): TranslatorInterface return $this->translator; } - /** - * @param TranslatorInterface $translator - * @return $this - */ - public function setTranslator(TranslatorInterface $translator): static - { - $this->translator = $translator; - return $this; - } - /** * @return Environment */ @@ -415,16 +396,6 @@ public function getTemplating(): Environment return $this->templating; } - /** - * @param Environment $templating - * @return $this - */ - public function setTemplating(Environment $templating): static - { - $this->templating = $templating; - return $this; - } - /** * @return MailerInterface */ @@ -433,16 +404,6 @@ public function getMailer(): MailerInterface return $this->mailer; } - /** - * @param MailerInterface $mailer - * @return $this - */ - public function setMailer(MailerInterface $mailer): static - { - $this->mailer = $mailer; - return $this; - } - /** * @return string|null */ diff --git a/src/Message/ApplyRealmNodeInheritanceMessage.php b/src/Message/ApplyRealmNodeInheritanceMessage.php index 7159e176..7943f8a8 100644 --- a/src/Message/ApplyRealmNodeInheritanceMessage.php +++ b/src/Message/ApplyRealmNodeInheritanceMessage.php @@ -6,27 +6,27 @@ final class ApplyRealmNodeInheritanceMessage implements AsyncMessage { - private int $nodeId; - private ?int $realmId; + private int|string|null $nodeId; + private int|string|null $realmId; - public function __construct(int $nodeId, ?int $realmId) + public function __construct(int|string|null $nodeId, int|string|null $realmId) { $this->nodeId = $nodeId; $this->realmId = $realmId; } /** - * @return int + * @return int|string|null */ - public function getNodeId(): int + public function getNodeId(): int|string|null { return $this->nodeId; } /** - * @return int|null + * @return int|string|null */ - public function getRealmId(): ?int + public function getRealmId(): int|string|null { return $this->realmId; } diff --git a/src/Message/CleanRealmNodeInheritanceMessage.php b/src/Message/CleanRealmNodeInheritanceMessage.php index 05579c30..cf1d1201 100644 --- a/src/Message/CleanRealmNodeInheritanceMessage.php +++ b/src/Message/CleanRealmNodeInheritanceMessage.php @@ -6,27 +6,27 @@ final class CleanRealmNodeInheritanceMessage implements AsyncMessage { - private int $nodeId; - private ?int $realmId; + private int|string|null $nodeId; + private int|string|null $realmId; - public function __construct(int $nodeId, ?int $realmId) + public function __construct(int|string|null $nodeId, int|string|null $realmId) { $this->nodeId = $nodeId; $this->realmId = $realmId; } /** - * @return int + * @return int|string|null */ - public function getNodeId(): int + public function getNodeId(): int|string|null { return $this->nodeId; } /** - * @return int|null + * @return int|string|null */ - public function getRealmId(): ?int + public function getRealmId(): int|string|null { return $this->realmId; } diff --git a/src/Message/DeleteNodeTypeMessage.php b/src/Message/DeleteNodeTypeMessage.php index a5f3c682..0f3caca1 100644 --- a/src/Message/DeleteNodeTypeMessage.php +++ b/src/Message/DeleteNodeTypeMessage.php @@ -6,20 +6,20 @@ final class DeleteNodeTypeMessage implements AsyncMessage { - private int $nodeTypeId; + private int|string|null $nodeTypeId; /** - * @param int $nodeTypeId + * @param int|string|null $nodeTypeId */ - public function __construct(int $nodeTypeId) + public function __construct(int|string|null $nodeTypeId) { $this->nodeTypeId = $nodeTypeId; } /** - * @return int + * @return int|string|null */ - public function getNodeTypeId(): int + public function getNodeTypeId(): int|string|null { return $this->nodeTypeId; } diff --git a/src/Message/Handler/ApplyRealmNodeInheritanceMessageHandler.php b/src/Message/Handler/ApplyRealmNodeInheritanceMessageHandler.php index 785eb73d..076d5692 100644 --- a/src/Message/Handler/ApplyRealmNodeInheritanceMessageHandler.php +++ b/src/Message/Handler/ApplyRealmNodeInheritanceMessageHandler.php @@ -17,13 +17,8 @@ final class ApplyRealmNodeInheritanceMessageHandler implements MessageHandlerInterface { - private ManagerRegistry $managerRegistry; - private HandlerFactoryInterface $handlerFactory; - - public function __construct(ManagerRegistry $managerRegistry, HandlerFactoryInterface $handlerFactory) + public function __construct(private readonly ManagerRegistry $managerRegistry) { - $this->managerRegistry = $managerRegistry; - $this->handlerFactory = $handlerFactory; } public function __invoke(ApplyRealmNodeInheritanceMessage $message): void @@ -53,15 +48,12 @@ public function __invoke(ApplyRealmNodeInheritanceMessage $message): void return; } - /** @var NodeHandler $nodeHandler */ - $nodeHandler = $this->handlerFactory->getHandler($node); - $childrenIds = $nodeHandler->getAllOffspringId(); + $nodeRepository = $this->managerRegistry->getRepository(Node::class); + $childrenIds = $nodeRepository->findAllOffspringIdByNode($node); foreach ($childrenIds as $childId) { /** @var Node|null $child */ - $child = $this->managerRegistry - ->getRepository(Node::class) - ->find($childId); + $child = $nodeRepository->find($childId); if (null === $child) { continue; } diff --git a/src/Message/Handler/CleanRealmNodeInheritanceMessageHandler.php b/src/Message/Handler/CleanRealmNodeInheritanceMessageHandler.php index b9d01fd6..90ad07da 100644 --- a/src/Message/Handler/CleanRealmNodeInheritanceMessageHandler.php +++ b/src/Message/Handler/CleanRealmNodeInheritanceMessageHandler.php @@ -16,13 +16,8 @@ final class CleanRealmNodeInheritanceMessageHandler implements MessageHandlerInterface { - private ManagerRegistry $managerRegistry; - private HandlerFactoryInterface $handlerFactory; - - public function __construct(ManagerRegistry $managerRegistry, HandlerFactoryInterface $handlerFactory) + public function __construct(private readonly ManagerRegistry $managerRegistry) { - $this->managerRegistry = $managerRegistry; - $this->handlerFactory = $handlerFactory; } public function __invoke(CleanRealmNodeInheritanceMessage $message): void @@ -40,9 +35,8 @@ public function __invoke(CleanRealmNodeInheritanceMessage $message): void throw new UnrecoverableMessageHandlingException('Realm does not exist'); } - /** @var NodeHandler $nodeHandler */ - $nodeHandler = $this->handlerFactory->getHandler($node); - $childrenIds = $nodeHandler->getAllOffspringId(); + $nodeRepository = $this->managerRegistry->getRepository(Node::class); + $childrenIds = $nodeRepository->findAllOffspringIdByNode($node); $realmNodes = $this->managerRegistry ->getRepository(RealmNode::class) diff --git a/src/Message/Handler/DeleteNodeTypeMessageHandler.php b/src/Message/Handler/DeleteNodeTypeMessageHandler.php index 6159b918..9a9c4723 100644 --- a/src/Message/Handler/DeleteNodeTypeMessageHandler.php +++ b/src/Message/Handler/DeleteNodeTypeMessageHandler.php @@ -19,15 +19,11 @@ final class DeleteNodeTypeMessageHandler implements MessageHandlerInterface { - private ManagerRegistry $managerRegistry; - private HandlerFactoryInterface $handlerFactory; - private MessageBusInterface $messageBus; - - public function __construct(ManagerRegistry $managerRegistry, HandlerFactoryInterface $handlerFactory, MessageBusInterface $messageBus) - { - $this->managerRegistry = $managerRegistry; - $this->handlerFactory = $handlerFactory; - $this->messageBus = $messageBus; + public function __construct( + private readonly ManagerRegistry $managerRegistry, + private readonly HandlerFactoryInterface $handlerFactory, + private readonly MessageBusInterface $messageBus + ) { } /** diff --git a/src/Message/Handler/HttpRequestMessageHandler.php b/src/Message/Handler/HttpRequestMessageHandler.php index b7848ed5..35f275e5 100644 --- a/src/Message/Handler/HttpRequestMessageHandler.php +++ b/src/Message/Handler/HttpRequestMessageHandler.php @@ -7,24 +7,15 @@ use GuzzleHttp\Client; use GuzzleHttp\Exception\GuzzleException; use Psr\Log\LoggerInterface; -use Psr\Log\NullLogger; use RZ\Roadiz\CoreBundle\Message\HttpRequestMessage; use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException; use Symfony\Component\Messenger\Handler\MessageHandlerInterface; final class HttpRequestMessageHandler implements MessageHandlerInterface { - private LoggerInterface $logger; - private ?Client $client; - - /** - * @param Client|null $client - * @param LoggerInterface|null $logger - */ - public function __construct(Client $client = null, ?LoggerInterface $logger = null) - { - $this->logger = $logger ?? new NullLogger(); - $this->client = $client ?? new Client(); + public function __construct( + private readonly LoggerInterface $logger + ) { } public function __invoke(HttpRequestMessage $message): void @@ -35,7 +26,8 @@ public function __invoke(HttpRequestMessage $message): void $message->getRequest()->getMethod(), $message->getRequest()->getUri() )); - $this->client->send($message->getRequest(), $message->getOptions()); + $client = new Client(); + $client->send($message->getRequest(), $message->getOptions()); } catch (GuzzleException $exception) { throw new UnrecoverableMessageHandlingException($exception->getMessage(), 0, $exception); } diff --git a/src/Message/Handler/PurgeReverseProxyCacheMessageHandler.php b/src/Message/Handler/PurgeReverseProxyCacheMessageHandler.php index 6082040d..13da36f7 100644 --- a/src/Message/Handler/PurgeReverseProxyCacheMessageHandler.php +++ b/src/Message/Handler/PurgeReverseProxyCacheMessageHandler.php @@ -20,27 +20,12 @@ final class PurgeReverseProxyCacheMessageHandler implements MessageHandlerInterface { - private UrlGeneratorInterface $urlGenerator; - private ReverseProxyCacheLocator $reverseProxyCacheLocator; - private MessageBusInterface $bus; - private ManagerRegistry $managerRegistry; - - /** - * @param MessageBusInterface $bus - * @param UrlGeneratorInterface $urlGenerator - * @param ReverseProxyCacheLocator $reverseProxyCacheLocator - * @param ManagerRegistry $managerRegistry - */ public function __construct( - MessageBusInterface $bus, - UrlGeneratorInterface $urlGenerator, - ReverseProxyCacheLocator $reverseProxyCacheLocator, - ManagerRegistry $managerRegistry + private readonly MessageBusInterface $bus, + private readonly UrlGeneratorInterface $urlGenerator, + private readonly ReverseProxyCacheLocator $reverseProxyCacheLocator, + private readonly ManagerRegistry $managerRegistry ) { - $this->urlGenerator = $urlGenerator; - $this->reverseProxyCacheLocator = $reverseProxyCacheLocator; - $this->managerRegistry = $managerRegistry; - $this->bus = $bus; } public function __invoke(PurgeReverseProxyCacheMessage $message): void diff --git a/src/Message/Handler/SearchRealmNodeInheritanceMessageHandler.php b/src/Message/Handler/SearchRealmNodeInheritanceMessageHandler.php index 145bc5e8..277bf6a5 100644 --- a/src/Message/Handler/SearchRealmNodeInheritanceMessageHandler.php +++ b/src/Message/Handler/SearchRealmNodeInheritanceMessageHandler.php @@ -21,21 +21,12 @@ final class SearchRealmNodeInheritanceMessageHandler implements MessageHandlerInterface { - private ManagerRegistry $managerRegistry; - private HandlerFactoryInterface $handlerFactory; - private MessageBusInterface $bus; - private LoggerInterface $logger; - public function __construct( - ManagerRegistry $managerRegistry, - HandlerFactoryInterface $handlerFactory, - MessageBusInterface $bus, - LoggerInterface $logger + private readonly ManagerRegistry $managerRegistry, + private readonly HandlerFactoryInterface $handlerFactory, + private readonly MessageBusInterface $bus, + private readonly LoggerInterface $logger ) { - $this->managerRegistry = $managerRegistry; - $this->handlerFactory = $handlerFactory; - $this->bus = $bus; - $this->logger = $logger; } public function __invoke(SearchRealmNodeInheritanceMessage $message): void @@ -65,7 +56,7 @@ private function clearAnyExistingRealmNodes(Node $node): void $this->logger->info('Clean existing RealmNode information'); $this->bus->dispatch(new Envelope(new CleanRealmNodeInheritanceMessage( $autoRealmNode->getNode()->getId(), - null !== $autoRealmNode->getRealm() ? $autoRealmNode->getRealm()->getId() : null + $autoRealmNode->getRealm()->getId() ))); } } @@ -90,7 +81,7 @@ private function applyRootRealmNodes(Node $node): void $this->logger->info('Apply new root RealmNode information'); $this->bus->dispatch(new Envelope(new ApplyRealmNodeInheritanceMessage( $rootRealmNode->getNode()->getId(), - null !== $rootRealmNode->getRealm() ? $rootRealmNode->getRealm()->getId() : null + $rootRealmNode->getRealm()->getId() ))); } } diff --git a/src/Message/Handler/UpdateDoctrineSchemaMessageHandler.php b/src/Message/Handler/UpdateDoctrineSchemaMessageHandler.php index e9c384f5..e9fc413d 100644 --- a/src/Message/Handler/UpdateDoctrineSchemaMessageHandler.php +++ b/src/Message/Handler/UpdateDoctrineSchemaMessageHandler.php @@ -10,11 +10,8 @@ final class UpdateDoctrineSchemaMessageHandler implements MessageHandlerInterface { - private SchemaUpdater $schemaUpdater; - - public function __construct(SchemaUpdater $schemaUpdater) + public function __construct(private readonly SchemaUpdater $schemaUpdater) { - $this->schemaUpdater = $schemaUpdater; } /** diff --git a/src/Message/Handler/UpdateNodeTypeSchemaMessageHandler.php b/src/Message/Handler/UpdateNodeTypeSchemaMessageHandler.php index 6a0185ec..a14b07d1 100644 --- a/src/Message/Handler/UpdateNodeTypeSchemaMessageHandler.php +++ b/src/Message/Handler/UpdateNodeTypeSchemaMessageHandler.php @@ -17,15 +17,11 @@ final class UpdateNodeTypeSchemaMessageHandler implements MessageHandlerInterface { - private ManagerRegistry $managerRegistry; - private HandlerFactoryInterface $handlerFactory; - private MessageBusInterface $messageBus; - - public function __construct(ManagerRegistry $managerRegistry, HandlerFactoryInterface $handlerFactory, MessageBusInterface $messageBus) - { - $this->managerRegistry = $managerRegistry; - $this->handlerFactory = $handlerFactory; - $this->messageBus = $messageBus; + public function __construct( + private readonly ManagerRegistry $managerRegistry, + private readonly HandlerFactoryInterface $handlerFactory, + private readonly MessageBusInterface $messageBus + ) { } public function __invoke(UpdateNodeTypeSchemaMessage $message): void diff --git a/src/Message/PurgeReverseProxyCacheMessage.php b/src/Message/PurgeReverseProxyCacheMessage.php index 58e79384..be0c54c0 100644 --- a/src/Message/PurgeReverseProxyCacheMessage.php +++ b/src/Message/PurgeReverseProxyCacheMessage.php @@ -6,20 +6,20 @@ final class PurgeReverseProxyCacheMessage implements AsyncMessage { - private int $nodeSourceId; + private int|string|null $nodeSourceId; /** - * @param int $nodeSourceId + * @param int|string|null $nodeSourceId */ - public function __construct(int $nodeSourceId) + public function __construct(int|string|null $nodeSourceId) { $this->nodeSourceId = $nodeSourceId; } /** - * @return int + * @return int|string|null */ - public function getNodeSourceId(): int + public function getNodeSourceId(): int|string|null { return $this->nodeSourceId; } diff --git a/src/Message/SearchRealmNodeInheritanceMessage.php b/src/Message/SearchRealmNodeInheritanceMessage.php index e3732026..c9ec45ff 100644 --- a/src/Message/SearchRealmNodeInheritanceMessage.php +++ b/src/Message/SearchRealmNodeInheritanceMessage.php @@ -6,17 +6,17 @@ final class SearchRealmNodeInheritanceMessage implements AsyncMessage { - private int $nodeId; + private int|string|null $nodeId; - public function __construct(int $nodeId) + public function __construct(int|string|null $nodeId) { $this->nodeId = $nodeId; } /** - * @return int + * @return int|string|null */ - public function getNodeId(): int + public function getNodeId(): int|string|null { return $this->nodeId; } diff --git a/src/Message/UpdateNodeTypeSchemaMessage.php b/src/Message/UpdateNodeTypeSchemaMessage.php index 830828b5..f4a309a5 100644 --- a/src/Message/UpdateNodeTypeSchemaMessage.php +++ b/src/Message/UpdateNodeTypeSchemaMessage.php @@ -9,20 +9,20 @@ */ final class UpdateNodeTypeSchemaMessage { - private int $nodeTypeId; + private int|string|null $nodeTypeId; /** - * @param int $nodeTypeId + * @param int|string|null $nodeTypeId */ - public function __construct(int $nodeTypeId) + public function __construct(int|string|null $nodeTypeId) { $this->nodeTypeId = $nodeTypeId; } /** - * @return int + * @return int|string|null */ - public function getNodeTypeId(): int + public function getNodeTypeId(): int|string|null { return $this->nodeTypeId; } diff --git a/src/Model/AttributeGroupTrait.php b/src/Model/AttributeGroupTrait.php index 6348ad92..398d03d5 100644 --- a/src/Model/AttributeGroupTrait.php +++ b/src/Model/AttributeGroupTrait.php @@ -17,10 +17,11 @@ trait AttributeGroupTrait { #[ - ORM\Column(name: "canonical_name", type: "string", unique: true, nullable: false), + ORM\Column(name: "canonical_name", type: "string", length: 255, unique: true, nullable: false), Serializer\Groups(["attribute_group", "attribute", "node", "nodes_sources"]), Serializer\Type("string"), Assert\NotNull(), + Assert\Length(max: 255), Assert\NotBlank() ] protected string $canonicalName = ''; @@ -31,7 +32,7 @@ trait AttributeGroupTrait #[ ORM\OneToMany(mappedBy: "group", targetEntity: AttributeInterface::class), Serializer\Groups(["attribute_group"]), - Serializer\Type("ArrayCollection") + Serializer\Type("ArrayCollection") ] protected Collection $attributes; @@ -39,9 +40,14 @@ trait AttributeGroupTrait * @var Collection */ #[ - ORM\OneToMany(mappedBy: "attributeGroup", targetEntity: AttributeGroupTranslationInterface::class, cascade: ["all"]), + ORM\OneToMany( + mappedBy: "attributeGroup", + targetEntity: AttributeGroupTranslationInterface::class, + cascade: ["all"], + orphanRemoval: true + ), Serializer\Groups(["attribute_group", "attribute", "node", "nodes_sources"]), - Serializer\Type("ArrayCollection"), + Serializer\Type("ArrayCollection"), Serializer\Accessor(getter: "getAttributeGroupTranslations", setter: "setAttributeGroupTranslations") ] protected Collection $attributeGroupTranslations; diff --git a/src/Model/AttributeGroupTranslationTrait.php b/src/Model/AttributeGroupTranslationTrait.php index 241fdb31..34ec8324 100644 --- a/src/Model/AttributeGroupTranslationTrait.php +++ b/src/Model/AttributeGroupTranslationTrait.php @@ -7,31 +7,33 @@ use Doctrine\ORM\Mapping as ORM; use JMS\Serializer\Annotation as Serializer; use RZ\Roadiz\Core\AbstractEntities\TranslationInterface; +use Symfony\Component\Validator\Constraints as Assert; trait AttributeGroupTranslationTrait { #[ ORM\ManyToOne(targetEntity: "RZ\Roadiz\Core\AbstractEntities\TranslationInterface"), - ORM\JoinColumn(name: "translation_id", onDelete: "CASCADE"), + ORM\JoinColumn(name: "translation_id", referencedColumnName: "id", nullable: false, onDelete: "CASCADE"), Serializer\Groups(["attribute_group", "attribute", "node", "nodes_sources"]), Serializer\Type("RZ\Roadiz\Core\AbstractEntities\TranslationInterface"), Serializer\Accessor(getter: "getTranslation", setter: "setTranslation") ] - protected ?TranslationInterface $translation = null; + protected TranslationInterface $translation; #[ - ORM\Column(type: "string", unique: false, nullable: false), + ORM\Column(type: "string", length: 255, unique: false, nullable: false), Serializer\Groups(["attribute_group", "attribute", "node", "nodes_sources"]), - Serializer\Type("string") + Serializer\Type("string"), + Assert\Length(max: 255) ] protected string $name = ''; #[ ORM\ManyToOne(targetEntity: AttributeGroupInterface::class, cascade: ["persist"], inversedBy: "attributeGroupTranslations"), - ORM\JoinColumn(name: "attribute_group_id", referencedColumnName: "id", nullable: true, onDelete: "CASCADE"), + ORM\JoinColumn(name: "attribute_group_id", referencedColumnName: "id", nullable: false, onDelete: "CASCADE"), Serializer\Exclude ] - protected ?AttributeGroupInterface $attributeGroup = null; + protected AttributeGroupInterface $attributeGroup; /** * @return string @@ -43,7 +45,6 @@ public function getName(): string /** * @param string $value - * * @return self */ public function setName(string $value) @@ -54,8 +55,7 @@ public function setName(string $value) /** * @param TranslationInterface $translation - * - * @return mixed + * @return self */ public function setTranslation(TranslationInterface $translation) { @@ -63,10 +63,7 @@ public function setTranslation(TranslationInterface $translation) return $this; } - /** - * @return TranslationInterface|null - */ - public function getTranslation(): ?TranslationInterface + public function getTranslation(): TranslationInterface { return $this->translation; } @@ -81,8 +78,7 @@ public function getAttributeGroup(): AttributeGroupInterface /** * @param AttributeGroupInterface $attributeGroup - * - * @return mixed + * @return self */ public function setAttributeGroup(AttributeGroupInterface $attributeGroup) { diff --git a/src/Model/AttributeInterface.php b/src/Model/AttributeInterface.php index f22e2ec2..1da81a62 100644 --- a/src/Model/AttributeInterface.php +++ b/src/Model/AttributeInterface.php @@ -129,6 +129,8 @@ public function getType(): int; */ public function getColor(): ?string; + public function getWeight(): int; + /** * @param string|null $color */ @@ -215,4 +217,7 @@ public function isEnum(): bool; * @return bool */ public function isCountry(): bool; + + public function getDefaultRealm(): ?RealmInterface; + public function setDefaultRealm(?RealmInterface $defaultRealm): self; } diff --git a/src/Model/AttributeTrait.php b/src/Model/AttributeTrait.php index 64cc7174..bb52d5e3 100644 --- a/src/Model/AttributeTrait.php +++ b/src/Model/AttributeTrait.php @@ -6,6 +6,8 @@ use Doctrine\Common\Collections\Collection; use RZ\Roadiz\Core\AbstractEntities\TranslationInterface; +use RZ\Roadiz\CoreBundle\Entity\AttributeGroup; +use RZ\Roadiz\CoreBundle\Entity\AttributeTranslation; use RZ\Roadiz\Utils\StringHandler; use Doctrine\ORM\Mapping as ORM; use JMS\Serializer\Annotation as Serializer; @@ -15,12 +17,13 @@ trait AttributeTrait { #[ - ORM\Column(type: "string", unique: true, nullable: false), + ORM\Column(type: "string", length: 255, unique: true, nullable: false), Serializer\Groups(["attribute", "node", "nodes_sources"]), SymfonySerializer\Groups(["attribute", "node", "nodes_sources"]), Serializer\Type("string"), Assert\NotNull(), - Assert\NotBlank() + Assert\NotBlank(), + Assert\Length(max: 255) ] protected string $code = ''; @@ -44,7 +47,8 @@ trait AttributeTrait ORM\Column(type: "string", length: 7, unique: false, nullable: true), Serializer\Groups(["attribute", "node", "nodes_sources"]), SymfonySerializer\Groups(["attribute", "node", "nodes_sources"]), - Serializer\Type("string") + Serializer\Type("string"), + Assert\Length(max: 7) ] protected ?string $color = null; @@ -58,12 +62,12 @@ trait AttributeTrait ORM\JoinColumn(name: "group_id", onDelete: "SET NULL"), Serializer\Groups(["attribute", "node", "nodes_sources"]), SymfonySerializer\Groups(["attribute", "node", "nodes_sources"]), - Serializer\Type("RZ\Roadiz\CoreBundle\Model\AttributeGroupInterface") + Serializer\Type(AttributeGroup::class) ] protected ?AttributeGroupInterface $group = null; /** - * @var Collection + * @var Collection */ #[ ORM\OneToMany( @@ -75,7 +79,7 @@ trait AttributeTrait ), Serializer\Groups(["attribute", "node", "nodes_sources"]), SymfonySerializer\Groups(["attribute", "node", "nodes_sources"]), - Serializer\Type("ArrayCollection"), + Serializer\Type("ArrayCollection<" . AttributeTranslation::class . ">"), Serializer\Accessor(getter: "getAttributeTranslations", setter: "setAttributeTranslations") ] protected Collection $attributeTranslations; @@ -227,9 +231,9 @@ public function getOptions(TranslationInterface $translation): ?array function (AttributeTranslationInterface $attributeTranslation) use ($translation) { return $attributeTranslation->getTranslation() === $translation; } - ); - if ($attributeTranslation->count() > 0) { - return $attributeTranslation->first()->getOptions(); + )->first(); + if (false !== $attributeTranslation) { + return $attributeTranslation->getOptions(); } return null; @@ -344,4 +348,26 @@ public function isCountry(): bool { return $this->getType() === AttributeInterface::COUNTRY_T; } + + public function getTypeLabel(): string + { + return match ($this->getType()) { + AttributeInterface::DATETIME_T => 'attributes.form.type.datetime', + AttributeInterface::BOOLEAN_T => 'attributes.form.type.boolean', + AttributeInterface::INTEGER_T => 'attributes.form.type.integer', + AttributeInterface::DECIMAL_T => 'attributes.form.type.decimal', + AttributeInterface::PERCENT_T => 'attributes.form.type.percent', + AttributeInterface::EMAIL_T => 'attributes.form.type.email', + AttributeInterface::COLOUR_T => 'attributes.form.type.colour', + AttributeInterface::ENUM_T => 'attributes.form.type.enum', + AttributeInterface::DATE_T => 'attributes.form.type.date', + AttributeInterface::COUNTRY_T => 'attributes.form.type.country', + default => 'attributes.form.type.string', + }; + } + + public function getAttributeValues(): Collection + { + return $this->attributeValues; + } } diff --git a/src/Model/AttributeTranslationTrait.php b/src/Model/AttributeTranslationTrait.php index 0c2cd860..9926436f 100644 --- a/src/Model/AttributeTranslationTrait.php +++ b/src/Model/AttributeTranslationTrait.php @@ -4,25 +4,27 @@ namespace RZ\Roadiz\CoreBundle\Model; -use JMS\Serializer\Annotation as Serializer; use Doctrine\ORM\Mapping as ORM; +use JMS\Serializer\Annotation as Serializer; use RZ\Roadiz\Core\AbstractEntities\TranslationInterface; +use Symfony\Component\Validator\Constraints as Assert; trait AttributeTranslationTrait { #[ ORM\ManyToOne(targetEntity: TranslationInterface::class), - ORM\JoinColumn(onDelete: "CASCADE"), + ORM\JoinColumn(name: "translation_id", referencedColumnName: "id", nullable: false, onDelete: "CASCADE"), Serializer\Groups(["attribute", "node", "nodes_sources"]), Serializer\Type("RZ\Roadiz\Core\AbstractEntities\TranslationInterface"), Serializer\Accessor(getter: "getTranslation", setter: "setTranslation") ] - protected ?TranslationInterface $translation = null; + protected TranslationInterface $translation; #[ - ORM\Column(type: "string", unique: false, nullable: false), + ORM\Column(type: "string", length: 250, unique: false, nullable: false), Serializer\Groups(["attribute", "node", "nodes_sources"]), - Serializer\Type("string") + Serializer\Type("string"), + Assert\Length(max: 250) ] protected string $label = ''; @@ -38,10 +40,10 @@ trait AttributeTranslationTrait #[ ORM\ManyToOne(targetEntity: AttributeInterface::class, cascade: ["persist"], inversedBy: "attributeTranslations"), - ORM\JoinColumn(referencedColumnName: "id", onDelete: "CASCADE"), + ORM\JoinColumn(name: "attribute_id", referencedColumnName: "id", nullable: false, onDelete: "CASCADE"), Serializer\Exclude ] - protected ?AttributeInterface $attribute = null; + protected AttributeInterface $attribute; /** * @return string|null @@ -64,7 +66,6 @@ public function setLabel(?string $label) /** * @param TranslationInterface $translation - * * @return $this */ public function setTranslation(TranslationInterface $translation) @@ -73,10 +74,7 @@ public function setTranslation(TranslationInterface $translation) return $this; } - /** - * @return TranslationInterface|null - */ - public function getTranslation(): ?TranslationInterface + public function getTranslation(): TranslationInterface { return $this->translation; } @@ -91,7 +89,6 @@ public function getAttribute(): AttributeInterface /** * @param AttributeInterface $attribute - * * @return $this */ public function setAttribute(AttributeInterface $attribute) diff --git a/src/Model/AttributeValueInterface.php b/src/Model/AttributeValueInterface.php index 8bb43e55..aab86a8c 100644 --- a/src/Model/AttributeValueInterface.php +++ b/src/Model/AttributeValueInterface.php @@ -11,8 +11,11 @@ interface AttributeValueInterface extends PositionedInterface, PersistableInterface { + public function getRealm(): ?RealmInterface; + public function setRealm(?RealmInterface $realm): self; + /** - * @return AttributeInterface + * @return AttributeInterface|null */ public function getAttribute(): ?AttributeInterface; diff --git a/src/Model/AttributeValueTrait.php b/src/Model/AttributeValueTrait.php index fd6b0cbe..ef154cfc 100644 --- a/src/Model/AttributeValueTrait.php +++ b/src/Model/AttributeValueTrait.php @@ -9,18 +9,19 @@ use Doctrine\ORM\Mapping as ORM; use JMS\Serializer\Annotation as Serializer; use RZ\Roadiz\Core\AbstractEntities\TranslationInterface; -use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter as BaseFilter; +use ApiPlatform\Doctrine\Orm\Filter as BaseFilter; trait AttributeValueTrait { #[ ORM\ManyToOne(targetEntity: AttributeInterface::class, fetch: "EAGER", inversedBy: "attributeValues"), - ORM\JoinColumn(name: "attribute_id", referencedColumnName: "id", onDelete: "CASCADE"), + ORM\JoinColumn(name: "attribute_id", referencedColumnName: "id", nullable: false, onDelete: "CASCADE"), Serializer\Groups(["attribute", "node", "nodes_sources"]), Serializer\Type("RZ\Roadiz\CoreBundle\Entity\Attribute"), ApiFilter(BaseFilter\SearchFilter::class, properties: [ "attribute.id" => "exact", "attribute.code" => "exact", + "attribute.color" => "exact", "attribute.type" => "exact", "attribute.group" => "exact", "attribute.group.canonicalName" => "exact", @@ -28,9 +29,16 @@ trait AttributeValueTrait ApiFilter(BaseFilter\BooleanFilter::class, properties: [ "attribute.visible", "attribute.searchable" + ]), + ApiFilter(BaseFilter\ExistsFilter::class, properties: [ + "attribute.color", + "attribute.group" + ]), + ApiFilter(BaseFilter\OrderFilter::class, properties: [ + "attribute.weight" => "DESC", ]) ] - protected ?AttributeInterface $attribute = null; + protected AttributeInterface $attribute; /** * @var Collection @@ -45,12 +53,21 @@ trait AttributeValueTrait ), Serializer\Groups(["attribute", "node", "nodes_sources"]), Serializer\Type("ArrayCollection"), - Serializer\Accessor(getter: "getAttributeValueTranslations", setter: "setAttributeValueTranslations") + Serializer\Accessor(getter: "getAttributeValueTranslations", setter: "setAttributeValueTranslations"), + ApiFilter(BaseFilter\SearchFilter::class, properties: [ + "attributeValueTranslations.value" => "partial", + ]), + ApiFilter(BaseFilter\RangeFilter::class, properties: [ + "attributeValueTranslations.value", + ]), + ApiFilter(BaseFilter\ExistsFilter::class, properties: [ + "attributeValueTranslations.value", + ]), ] protected Collection $attributeValueTranslations; /** - * @return AttributeInterface + * @return AttributeInterface|null */ public function getAttribute(): ?AttributeInterface { @@ -59,8 +76,7 @@ public function getAttribute(): ?AttributeInterface /** * @param AttributeInterface $attribute - * - * @return mixed + * @return self */ public function setAttribute(AttributeInterface $attribute) { @@ -87,7 +103,7 @@ public function getAttributeValueTranslations(): Collection /** * @param Collection $attributeValueTranslations * - * @return mixed + * @return static */ public function setAttributeValueTranslations(Collection $attributeValueTranslations) { @@ -96,13 +112,13 @@ public function setAttributeValueTranslations(Collection $attributeValueTranslat foreach ($this->attributeValueTranslations as $attributeValueTranslation) { $attributeValueTranslation->setAttributeValue($this); } - return true; + return $this; } /** * @param TranslationInterface $translation * - * @return AttributeValueTranslationInterface + * @return AttributeValueTranslationInterface|null */ public function getAttributeValueTranslation(TranslationInterface $translation): ?AttributeValueTranslationInterface { @@ -115,4 +131,16 @@ public function getAttributeValueTranslation(TranslationInterface $translation): }) ->first() ?: null; } + + /** + * @return AttributeValueTranslationInterface|null + */ + public function getAttributeValueDefaultTranslation(): ?AttributeValueTranslationInterface + { + return $this->getAttributeValueTranslations() + ->filter(function (AttributeValueTranslationInterface $attributeValueTranslation) { + return $attributeValueTranslation->getTranslation()?->isDefaultTranslation() ?? false; + }) + ->first() ?: null; + } } diff --git a/src/Model/AttributeValueTranslationTrait.php b/src/Model/AttributeValueTranslationTrait.php index 94020752..57cb8ae2 100644 --- a/src/Model/AttributeValueTranslationTrait.php +++ b/src/Model/AttributeValueTranslationTrait.php @@ -7,62 +7,58 @@ use RZ\Roadiz\Core\AbstractEntities\TranslationInterface; use Doctrine\ORM\Mapping as ORM; use JMS\Serializer\Annotation as Serializer; +use Symfony\Component\Validator\Constraints as Assert; trait AttributeValueTranslationTrait { #[ ORM\ManyToOne(targetEntity: TranslationInterface::class), - ORM\JoinColumn(name: "translation_id", referencedColumnName: "id", onDelete: "CASCADE"), + ORM\JoinColumn(name: "translation_id", referencedColumnName: "id", nullable: false, onDelete: "CASCADE"), Serializer\Groups(["attribute", "node", "nodes_sources"]), Serializer\Type("RZ\Roadiz\Core\AbstractEntities\TranslationInterface"), Serializer\Accessor(getter: "getTranslation", setter: "setTranslation") ] - protected ?TranslationInterface $translation = null; + protected TranslationInterface $translation; #[ ORM\Column(type: "string", length: 255, unique: false, nullable: true), Serializer\Groups(["attribute", "node", "nodes_sources"]), - Serializer\Type("string") + Serializer\Type("string"), + Assert\Length(max: 255) ] protected ?string $value = null; #[ ORM\ManyToOne(targetEntity: AttributeValueInterface::class, cascade: ["persist"], inversedBy: "attributeValueTranslations"), - ORM\JoinColumn(name: "attribute_value", referencedColumnName: "id", onDelete: "CASCADE"), + ORM\JoinColumn(name: "attribute_value", referencedColumnName: "id", nullable: false, onDelete: "CASCADE"), Serializer\Exclude ] - protected ?AttributeValueInterface $attributeValue = null; + protected AttributeValueInterface $attributeValue; /** - * @return mixed|null + * @return bool|\DateTime|float|int|string|null * @throws \Exception */ - public function getValue() + public function getValue(): bool|\DateTime|float|int|string|null { if (null === $this->value) { return null; } - switch ($this->getAttributeValue()->getType()) { - case AttributeInterface::DECIMAL_T: - return (float) $this->value; - case AttributeInterface::INTEGER_T: - return (int) $this->value; - case AttributeInterface::BOOLEAN_T: - return (bool) $this->value; - case AttributeInterface::DATETIME_T: - case AttributeInterface::DATE_T: - return $this->value ? new \DateTime($this->value) : null; - default: - return $this->value; - } + return match ($this->getAttributeValue()->getType()) { + AttributeInterface::DECIMAL_T => (float) $this->value, + AttributeInterface::INTEGER_T => (int) $this->value, + AttributeInterface::BOOLEAN_T => (bool) $this->value, + AttributeInterface::DATETIME_T, AttributeInterface::DATE_T => $this->value ? new \DateTime($this->value) : null, + default => $this->value, + }; } /** * @param mixed|null $value * - * @return static + * @return self */ - public function setValue($value) + public function setValue(mixed $value) { if (null === $value) { $this->value = null; @@ -76,7 +72,7 @@ public function setValue($value) return $this; case AttributeInterface::DATETIME_T: case AttributeInterface::DATE_T: - if ($value instanceof \DateTime) { + if ($value instanceof \DateTimeInterface) { $this->value = $value->format('Y-m-d H:i:s'); } else { $this->value = (string) $value; @@ -90,8 +86,7 @@ public function setValue($value) /** * @param TranslationInterface $translation - * - * @return static + * @return self */ public function setTranslation(TranslationInterface $translation) { @@ -99,10 +94,7 @@ public function setTranslation(TranslationInterface $translation) return $this; } - /** - * @return TranslationInterface|null - */ - public function getTranslation(): ?TranslationInterface + public function getTranslation(): TranslationInterface { return $this->translation; } @@ -117,8 +109,7 @@ public function getAttributeValue(): AttributeValueInterface /** * @param AttributeValueInterface $attributeValue - * - * @return static + * @return self */ public function setAttributeValue(AttributeValueInterface $attributeValue) { @@ -126,10 +117,7 @@ public function setAttributeValue(AttributeValueInterface $attributeValue) return $this; } - /** - * @return AttributeInterface - */ - public function getAttribute(): ?AttributeInterface + public function getAttribute(): AttributeInterface { return $this->getAttributeValue()->getAttribute(); } diff --git a/src/Model/RealmInterface.php b/src/Model/RealmInterface.php index e637d108..6518eca8 100644 --- a/src/Model/RealmInterface.php +++ b/src/Model/RealmInterface.php @@ -5,8 +5,9 @@ namespace RZ\Roadiz\CoreBundle\Model; use Doctrine\Common\Collections\Collection; +use RZ\Roadiz\Core\AbstractEntities\PersistableInterface; -interface RealmInterface +interface RealmInterface extends PersistableInterface { public const TYPE_PLAIN_PASSWORD = 'plain_password'; public const TYPE_ROLE = 'bearer_role'; diff --git a/src/Node/NodeDuplicator.php b/src/Node/NodeDuplicator.php index 21e6c091..da995e69 100644 --- a/src/Node/NodeDuplicator.php +++ b/src/Node/NodeDuplicator.php @@ -79,8 +79,12 @@ public function duplicate(): Node */ private function doDuplicate(Node &$node): Node { + $nodeSource = $node->getNodeSources()->first(); + if (false === $nodeSource) { + throw new \RuntimeException('Node source is missing.'); + } $node->setNodeName( - $this->nodeNamePolicy->getSafeNodeName($node->getNodeSources()->first()) + $this->nodeNamePolicy->getSafeNodeName($nodeSource) ); /** @var Node $child */ @@ -98,8 +102,7 @@ private function doDuplicate(Node &$node): Node $nsDoc->setNodeSource($nodeSource); $doc = $nsDoc->getDocument(); $nsDoc->setDocument($doc); - $f = $nsDoc->getField(); - $nsDoc->setField($f); + $nsDoc->setFieldName($nsDoc->getFieldName()); $this->objectManager->persist($nsDoc); } } @@ -137,9 +140,11 @@ private function doDuplicate(Node &$node): Node */ private function doDuplicateNodeRelations(Node $node): Node { - $nodeRelations = new ArrayCollection($node->getBNodes()->toArray()); + /** @var NodesToNodes[] $nodeRelations */ + $nodeRelations = $node->getBNodes()->toArray(); foreach ($nodeRelations as $position => $nodeRelation) { - $ntn = new NodesToNodes($node, $nodeRelation->getNodeB(), $nodeRelation->getField()); + $ntn = new NodesToNodes($node, $nodeRelation->getNodeB()); + $ntn->setFieldName($nodeRelation->getFieldName()); $ntn->setPosition($position); $this->objectManager->persist($ntn); } diff --git a/src/Node/NodeFactory.php b/src/Node/NodeFactory.php index 47b33cbb..1cbc852b 100644 --- a/src/Node/NodeFactory.php +++ b/src/Node/NodeFactory.php @@ -17,19 +17,10 @@ final class NodeFactory { - private ManagerRegistry $managerRegistry; - private NodeNamePolicyInterface $nodeNamePolicy; - - /** - * @param ManagerRegistry $managerRegistry - * @param NodeNamePolicyInterface $nodeNamePolicy - */ public function __construct( - ManagerRegistry $managerRegistry, - NodeNamePolicyInterface $nodeNamePolicy + private readonly ManagerRegistry $managerRegistry, + private readonly NodeNamePolicyInterface $nodeNamePolicy ) { - $this->nodeNamePolicy = $nodeNamePolicy; - $this->managerRegistry = $managerRegistry; } public function create( @@ -52,12 +43,14 @@ public function create( } if ($node === null) { - $node = new Node($type); + $node = new Node(); + $node->setNodeType($type); } - if ($type instanceof NodeType) { - $node->setTtl($type->getDefaultTtl()); + if ($node->getNodeType() instanceof NodeType) { + $node->setTtl($node->getNodeType()->getDefaultTtl()); } + if (null !== $parent) { $node->setParent($parent); } @@ -100,10 +93,12 @@ public function createWithUrlAlias( ?Node $parent = null ): Node { $node = $this->create($title, $type, $translation, $node, $parent); + $nodeSource = $node->getNodeSources()->first(); /** @var UrlAliasRepository $repository */ $repository = $this->managerRegistry->getRepository(UrlAlias::class); - if (false === $repository->exists($urlAlias)) { - $alias = new UrlAlias($node->getNodeSources()->first()); + if (false !== $nodeSource && false === $repository->exists($urlAlias)) { + $alias = new UrlAlias(); + $alias->setNodeSource($nodeSource); $alias->setAlias($urlAlias); $this->managerRegistry->getManagerForClass(UrlAlias::class)->persist($alias); } diff --git a/src/Node/NodeMover.php b/src/Node/NodeMover.php index 2e866db9..8deaff0d 100644 --- a/src/Node/NodeMover.php +++ b/src/Node/NodeMover.php @@ -14,6 +14,8 @@ use RZ\Roadiz\CoreBundle\Entity\Redirection; use RZ\Roadiz\Core\Handlers\HandlerFactoryInterface; use RZ\Roadiz\CoreBundle\EntityHandler\NodeHandler; +use RZ\Roadiz\CoreBundle\Event\Redirection\PostCreatedRedirectionEvent; +use RZ\Roadiz\CoreBundle\Event\Redirection\PostUpdatedRedirectionEvent; use RZ\Roadiz\CoreBundle\Repository\EntityRepository; use RZ\Roadiz\CoreBundle\Routing\NodeRouter; use RZ\Roadiz\CoreBundle\Node\Exception\SameNodeUrlException; @@ -201,6 +203,7 @@ protected function redirect(NodesSources $nodeSource, string $previousPath, bool $this->getManager()->remove($loopingRedirection); } + /** @var Redirection|null $existingRedirection */ $existingRedirection = $redirectionRepo->findOneBy([ 'query' => $previousPath, ]); @@ -210,7 +213,7 @@ protected function redirect(NodesSources $nodeSource, string $previousPath, bool $existingRedirection->setQuery($previousPath); $this->logger->info('New redirection created', [ 'oldPath' => $previousPath, - 'nodeSource' => $nodeSource->getId(), + 'nodeSource' => $nodeSource, ]); } $existingRedirection->setRedirectNodeSource($nodeSource); @@ -219,6 +222,7 @@ protected function redirect(NodesSources $nodeSource, string $previousPath, bool } else { $existingRedirection->setType(Response::HTTP_FOUND); } + $this->dispatcher->dispatch(new PostUpdatedRedirectionEvent($existingRedirection)); } return $nodeSource; diff --git a/src/Node/NodeNameChecker.php b/src/Node/NodeNameChecker.php index e1c4f98b..b9b54d77 100644 --- a/src/Node/NodeNameChecker.php +++ b/src/Node/NodeNameChecker.php @@ -53,7 +53,7 @@ public function getCanonicalNodeName(NodesSources $nodeSource): string return sprintf( '%s-%s', $nodeTypeSuffix, - null !== $nodeSource->getNode() ? $nodeSource->getNode()->getId() : $nodeSource->getId() + null !== $nodeSource->getNode()->getId() ); } diff --git a/src/Node/NodeTranstyper.php b/src/Node/NodeTranstyper.php index f4a20417..2109757e 100644 --- a/src/Node/NodeTranstyper.php +++ b/src/Node/NodeTranstyper.php @@ -13,7 +13,6 @@ use RZ\Roadiz\Contracts\NodeType\NodeTypeInterface; use RZ\Roadiz\Core\AbstractEntities\AbstractField; use RZ\Roadiz\Core\AbstractEntities\TranslationInterface; -use RZ\Roadiz\CoreBundle\Entity\Log; use RZ\Roadiz\CoreBundle\Entity\Node; use RZ\Roadiz\CoreBundle\Entity\NodesSources; use RZ\Roadiz\CoreBundle\Entity\NodesSourcesDocuments; @@ -98,6 +97,7 @@ public function transtype(Node $node, NodeTypeInterface $destinationNodeType, bo } $this->logger->debug('Get matching fields'); + /** @var class-string $sourceClass */ $sourceClass = $destinationNodeType->getSourceEntityFullQualifiedClassName(); /* @@ -113,14 +113,6 @@ public function transtype(Node $node, NodeTypeInterface $destinationNodeType, bo * Perform actual trans-typing */ $existingSources = $node->getNodeSources()->toArray(); - $existingLogs = []; - /** @var NodesSources $existingSource */ - foreach ($existingSources as $existingSource) { - $existingLogs[$existingSource->getTranslation()->getLocale()] = array_map(function (Log $log) { - $this->managerRegistry->getManager()->detach($log); - return $log; - }, $existingSource->getLogs()->toArray()); - } $existingRedirections = []; /** @var NodesSources $existingSource */ foreach ($existingSources as $existingSource) { @@ -141,7 +133,6 @@ public function transtype(Node $node, NodeTypeInterface $destinationNodeType, bo $existingSource->getTranslation(), $sourceClass, $fieldAssociations, - $existingLogs, $existingRedirections ); $this->logger->debug('Transtyped: ' . $existingSource->getTranslation()->getLocale()); @@ -174,9 +165,8 @@ protected function removeOldSources(Node $node, array &$sources): void * @param Node $node * @param NodesSources $existingSource * @param TranslationInterface $translation - * @param string $sourceClass + * @param class-string $sourceClass * @param array $fieldAssociations - * @param array $existingLogs * @param array $existingRedirections * @return NodesSources */ @@ -186,7 +176,6 @@ protected function doTranstypeSingleSource( TranslationInterface $translation, string $sourceClass, array &$fieldAssociations, - array &$existingLogs, array &$existingRedirections ): NodesSources { /** @var NodesSources $source */ @@ -213,7 +202,8 @@ protected function doTranstypeSingleSource( */ $documents = $existingSource->getDocumentsByFieldsWithName($oldField->getName()); foreach ($documents as $document) { - $nsDoc = new NodesSourcesDocuments($source, $document, $matchingField); + $nsDoc = new NodesSourcesDocuments($source, $document); + $nsDoc->setFieldName($matchingField->getName()); $this->getManager()->persist($nsDoc); $source->getDocumentsByFields()->add($nsDoc); } @@ -221,26 +211,13 @@ protected function doTranstypeSingleSource( } $this->logger->debug('Fill existing data'); - - /** @var Log $log */ - foreach ($existingLogs[$translation->getLocale()] as $log) { - $newLog = clone $log; - $newLog->setAdditionalData($log->getAdditionalData()); - $newLog->setChannel($log->getChannel()); - $newLog->setClientIp($log->getClientIp()); - $newLog->setUser($log->getUser()); - $newLog->setUsername($log->getUsername()); - $this->getManager()->persist($newLog); - $newLog->setNodeSource($source); - } - $this->logger->debug('Recreate logs'); - /* * Recreate url-aliases too. */ /** @var UrlAlias $urlAlias */ foreach ($existingSource->getUrlAliases() as $urlAlias) { - $newUrlAlias = new UrlAlias($source); + $newUrlAlias = new UrlAlias(); + $newUrlAlias->setNodeSource($source); $this->getManager()->persist($newUrlAlias); $newUrlAlias->setAlias($urlAlias->getAlias()); $source->addUrlAlias($newUrlAlias); @@ -282,6 +259,7 @@ protected function mockTranstype(NodeTypeInterface $nodeType): void * transtype, not to get an orphan node. */ $node = new Node(); + $node->setNodeType($nodeType); $node->setNodeName('testing_before_transtype' . $uniqueId); $this->getManager()->persist($node); diff --git a/src/Node/UniqueNodeGenerator.php b/src/Node/UniqueNodeGenerator.php index 3f3c6762..718cea75 100644 --- a/src/Node/UniqueNodeGenerator.php +++ b/src/Node/UniqueNodeGenerator.php @@ -4,6 +4,9 @@ namespace RZ\Roadiz\CoreBundle\Node; +use DateTime; +use Doctrine\ORM\OptimisticLockException; +use Doctrine\ORM\ORMException; use Doctrine\Persistence\ManagerRegistry; use RZ\Roadiz\Core\AbstractEntities\TranslationInterface; use RZ\Roadiz\CoreBundle\Entity\Node; @@ -11,22 +14,19 @@ use RZ\Roadiz\CoreBundle\Entity\NodeType; use RZ\Roadiz\CoreBundle\Entity\Tag; use RZ\Roadiz\CoreBundle\Entity\Translation; +use RZ\Roadiz\CoreBundle\Security\Authorization\Voter\NodeVoter; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; +use Symfony\Component\Security\Core\Exception\AccessDeniedException; +use Symfony\Bundle\SecurityBundle\Security; class UniqueNodeGenerator { - protected NodeNamePolicyInterface $nodeNamePolicy; - private ManagerRegistry $managerRegistry; - - /** - * @param ManagerRegistry $managerRegistry - * @param NodeNamePolicyInterface $nodeNamePolicy - */ - public function __construct(ManagerRegistry $managerRegistry, NodeNamePolicyInterface $nodeNamePolicy) - { - $this->nodeNamePolicy = $nodeNamePolicy; - $this->managerRegistry = $managerRegistry; + public function __construct( + protected ManagerRegistry $managerRegistry, + protected NodeNamePolicyInterface $nodeNamePolicy, + protected Security $security, + ) { } /** @@ -49,15 +49,14 @@ public function generate( bool $pushToTop = false ): NodesSources { $name = $nodeType->getDisplayName() . " " . uniqid(); - $node = new Node($nodeType); + $node = new Node(); + $node->setNodeType($nodeType); $node->setTtl($nodeType->getDefaultTtl()); if (null !== $tag) { $node->addTag($tag); } - if (null !== $parent) { - $parent->addChild($node); - } + $parent?->addChild($node); if ($pushToTop) { /* @@ -66,11 +65,12 @@ public function generate( $node->setPosition(0.5); } + /** @var class-string $sourceClass */ # phpstan hint $sourceClass = NodeType::getGeneratedEntitiesNamespace() . "\\" . $nodeType->getSourceEntityClassName(); $source = new $sourceClass($node, $translation); $source->setTitle($name); - $source->setPublishedAt(new \DateTime()); + $source->setPublishedAt(new DateTime()); $node->setNodeName($this->nodeNamePolicy->getCanonicalNodeName($source)); $manager = $this->managerRegistry->getManagerForClass(Node::class); @@ -89,8 +89,8 @@ public function generate( * @param Request $request * * @return NodesSources - * @throws \Doctrine\ORM\ORMException - * @throws \Doctrine\ORM\OptimisticLockException + * @throws ORMException + * @throws OptimisticLockException */ public function generateFromRequest(Request $request): NodesSources { @@ -112,7 +112,13 @@ public function generateFromRequest(Request $request): NodesSources $parent = $this->managerRegistry ->getRepository(Node::class) ->find((int) $request->get('parentNodeId')); + if (null === $parent || !$this->security->isGranted(NodeVoter::CREATE, $parent)) { + throw new BadRequestHttpException("Parent node does not exist."); + } } else { + if (!$this->security->isGranted(NodeVoter::CREATE_AT_ROOT)) { + throw new AccessDeniedException('You are not allowed to create a node at root.'); + } $parent = null; } @@ -139,8 +145,8 @@ public function generateFromRequest(Request $request): NodesSources /* * If parent has only on translation, use parent translation instead of default one. */ - if (null !== $parent && $parent->getNodeSources()->count() === 1) { - $translation = $parent->getNodeSources()->first()->getTranslation(); + if (null !== $parent && false !== $parentNodeSource = $parent->getNodeSources()->first()) { + $translation = $parentNodeSource->getTranslation(); } else { /** @var Translation $translation */ $translation = $this->managerRegistry diff --git a/src/Node/UniversalDataDuplicator.php b/src/Node/UniversalDataDuplicator.php index 41b0ae45..c806f3ae 100644 --- a/src/Node/UniversalDataDuplicator.php +++ b/src/Node/UniversalDataDuplicator.php @@ -5,6 +5,7 @@ namespace RZ\Roadiz\CoreBundle\Node; use Doctrine\Persistence\ManagerRegistry; +use RZ\Roadiz\Contracts\NodeType\NodeTypeFieldInterface; use RZ\Roadiz\Core\AbstractEntities\AbstractField; use RZ\Roadiz\CoreBundle\Entity\NodesSources; use RZ\Roadiz\CoreBundle\Entity\NodesSourcesDocuments; @@ -110,15 +111,10 @@ private function hasDefaultTranslation(NodesSources $source): bool return $sourceCount === 1; } - /** - * @param NodesSources $universalSource - * @param NodesSources $destSource - * @param NodeTypeField $field - */ protected function duplicateNonVirtualField( NodesSources $universalSource, NodesSources $destSource, - NodeTypeField $field + NodeTypeFieldInterface $field ): void { $getter = $field->getGetterName(); $setter = $field->getSetterName(); @@ -126,23 +122,18 @@ protected function duplicateNonVirtualField( $destSource->$setter($universalSource->$getter()); } - /** - * @param NodesSources $universalSource - * @param NodesSources $destSource - * @param NodeTypeField $field - */ protected function duplicateDocumentsField( NodesSources $universalSource, NodesSources $destSource, - NodeTypeField $field + NodeTypeFieldInterface $field ): void { $newDocuments = $this->managerRegistry ->getRepository(NodesSourcesDocuments::class) - ->findBy(['nodeSource' => $universalSource, 'field' => $field]); + ->findBy(['nodeSource' => $universalSource, 'fieldName' => $field->getName()]); $formerDocuments = $this->managerRegistry ->getRepository(NodesSourcesDocuments::class) - ->findBy(['nodeSource' => $destSource, 'field' => $field]); + ->findBy(['nodeSource' => $destSource, 'fieldName' => $field->getName()]); $manager = $this->managerRegistry->getManagerForClass(NodesSourcesDocuments::class); if (null === $manager) { diff --git a/src/NodeType/ApiResourceGenerator.php b/src/NodeType/ApiResourceGenerator.php index 59192eaa..6506c450 100644 --- a/src/NodeType/ApiResourceGenerator.php +++ b/src/NodeType/ApiResourceGenerator.php @@ -4,19 +4,32 @@ namespace RZ\Roadiz\CoreBundle\NodeType; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use Doctrine\Inflector\InflectorFactory; use LogicException; +use Psr\Log\LoggerInterface; use RZ\Roadiz\Contracts\NodeType\NodeTypeInterface; +use RZ\Roadiz\CoreBundle\Api\Controller\GetWebResponseByPathController; +use RZ\Roadiz\CoreBundle\Api\Model\WebResponseInterface; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\String\UnicodeString; use Symfony\Component\Yaml\Yaml; final class ApiResourceGenerator { - private string $apiResourcesDir; - - public function __construct(string $apiResourcesDir) - { - $this->apiResourcesDir = $apiResourcesDir; + /** + * @param ApiResourceOperationNameGenerator $apiResourceOperationNameGenerator + * @param string $apiResourcesDir + * @param LoggerInterface $logger + * @param class-string $webResponseClass + */ + public function __construct( + private readonly ApiResourceOperationNameGenerator $apiResourceOperationNameGenerator, + private readonly string $apiResourcesDir, + private readonly LoggerInterface $logger, + private readonly string $webResponseClass + ) { } /** @@ -32,12 +45,38 @@ public function generate(NodeTypeInterface $nodeType): ?string } $resourcePath = $this->getResourcePath($nodeType); + $webResponseResourcePath = $this->getWebResponseResourcePath(); + + if (!$filesystem->exists($webResponseResourcePath)) { + $filesystem->dumpFile( + $webResponseResourcePath, + Yaml::dump([ + 'resources' => [ + $this->webResponseClass => [ + 'operations' => [], + ] + ] + ], 7) + ); + } + $filesystem->dumpFile( + $webResponseResourcePath, + Yaml::dump($this->addWebResponseResourceOperation($nodeType, $webResponseResourcePath), 7) + ); + $this->logger->info('API WebResponse config file has been updated.', [ + 'file' => $webResponseResourcePath, + ]); + \clearstatcache(true, $webResponseResourcePath); if (!$filesystem->exists($resourcePath)) { $filesystem->dumpFile( $resourcePath, - Yaml::dump($this->getApiResourceDefinition($nodeType), 6) + Yaml::dump($this->getApiResourceDefinition($nodeType), 7) ); + $this->logger->info('API resource config file has been generated.', [ + 'nodeType' => $nodeType->getName(), + 'file' => $resourcePath, + ]); \clearstatcache(true, $resourcePath); return $resourcePath; } else { @@ -54,14 +93,30 @@ public function remove(NodeTypeInterface $nodeType): void } $resourcePath = $this->getResourcePath($nodeType); + $webResponseResourcePath = $this->getWebResponseResourcePath(); + + if ($filesystem->exists($webResponseResourcePath)) { + $filesystem->dumpFile( + $webResponseResourcePath, + Yaml::dump($this->removeWebResponseResourceOperation($nodeType, $webResponseResourcePath), 7) + ); + $this->logger->info('API WebResponse config file has been updated.', [ + 'file' => $webResponseResourcePath, + ]); + \clearstatcache(true, $webResponseResourcePath); + } if ($filesystem->exists($resourcePath)) { $filesystem->remove($resourcePath); + $this->logger->info('API resource config file has been removed.', [ + 'nodeType' => $nodeType->getName(), + 'file' => $resourcePath, + ]); @\clearstatcache(true, $resourcePath); } } - protected function getResourcePath(NodeTypeInterface $nodeType): string + public function getResourcePath(NodeTypeInterface $nodeType): string { return $this->apiResourcesDir . '/' . (new UnicodeString($nodeType->getName())) ->lower() @@ -70,22 +125,148 @@ protected function getResourcePath(NodeTypeInterface $nodeType): string ->toString(); } + protected function getWebResponseResourcePath(): string + { + return $this->apiResourcesDir . '/web_response.yml'; + } + + protected function getResourceName(string $nodeTypeName): string + { + return (new UnicodeString($nodeTypeName)) + ->snake() + ->lower() + ->toString(); + } + + protected function getResourceUriPrefix(NodeTypeInterface $nodeType): string + { + $pluralNodeTypeName = InflectorFactory::create()->build()->pluralize($nodeType->getName()); + return '/' . $this->getResourceName($pluralNodeTypeName); + } + protected function getApiResourceDefinition(NodeTypeInterface $nodeType): array { $fqcn = (new UnicodeString($nodeType->getSourceEntityFullQualifiedClassName())) ->trimStart('\\') ->toString(); - return [ $fqcn => [ - 'iri' => $nodeType->getName(), - 'shortName' => $nodeType->getName(), - 'collectionOperations' => $this->getCollectionOperations($nodeType), - 'itemOperations' => $this->getItemOperations($nodeType), - ]]; + return [ + 'resources' => [ + $fqcn => [ + 'shortName' => $nodeType->getName(), + 'types' => [$nodeType->getName()], + 'operations' => [ + ...$this->getCollectionOperations($nodeType), + ...$this->getItemOperations($nodeType) + ], + ] + ] + ]; + } + + protected function addWebResponseResourceOperation(NodeTypeInterface $nodeType, string $webResponseResourcePath): array + { + $getByPathOperationName = $this->apiResourceOperationNameGenerator->generateGetByPath( + $nodeType->getSourceEntityFullQualifiedClassName() + ); + $webResponseResource = Yaml::parseFile($webResponseResourcePath); + + if (!\array_key_exists($this->webResponseClass, $webResponseResource['resources'])) { + $webResponseResource = [ + 'resources' => [ + $this->webResponseClass => [ + 'operations' => [], + ] + ], + ]; + } + + if (\array_key_exists('operations', $webResponseResource['resources'][$this->webResponseClass])) { + $operations = $webResponseResource['resources'][$this->webResponseClass]['operations']; + } else { + $operations = []; + } + + if (!$nodeType->isReachable()) { + // Do not add operation if node-type is not reachable + return $webResponseResource; + } + if (\array_key_exists($getByPathOperationName, $operations)) { + // Do not add operation if already exists + return $webResponseResource; + } + + $groups = $this->getItemOperationSerializationGroups($nodeType); + $operations[$getByPathOperationName] = [ + 'method' => 'GET', + 'class' => Get::class, + 'uriTemplate' => '/web_response_by_path', + 'read' => false, + 'controller' => GetWebResponseByPathController::class, + 'normalizationContext' => [ + 'pagination_enabled' => false, + 'enable_max_depth' => true, + 'groups' => [ + $getByPathOperationName, + ...array_values(array_filter(array_unique($groups))), + ...[ + 'web_response', + 'walker', + 'children', + ] + ] + ], + 'openapiContext' => [ + 'tags' => ['WebResponse'], + 'summary' => 'Get a ' . $nodeType->getName() . ' by its path wrapped in a WebResponse object', + 'description' => 'Get a ' . $nodeType->getName() . ' by its path wrapped in a WebResponse', + 'parameters' => [ + [ + 'type' => 'string', + 'name' => 'path', + 'in' => 'query', + 'required' => true, + 'description' => 'Resource path, or `/` for home page', + 'schema' => [ + 'type' => 'string', + ], + ] + ] + ] + ]; + + $webResponseResource['resources'][$this->webResponseClass]['operations'] = $operations; + return $webResponseResource; + } + + protected function removeWebResponseResourceOperation(NodeTypeInterface $nodeType, string $webResponseResourcePath): array + { + $getByPathOperationName = $this->apiResourceOperationNameGenerator->generateGetByPath( + $nodeType->getSourceEntityFullQualifiedClassName() + ); + $webResponseResource = Yaml::parseFile($webResponseResourcePath); + + if (!\array_key_exists($this->webResponseClass, $webResponseResource['resources'])) { + return $webResponseResource; + } + if (\array_key_exists('operations', $webResponseResource['resources'][$this->webResponseClass])) { + $operations = $webResponseResource['resources'][$this->webResponseClass]['operations']; + } else { + return $webResponseResource; + } + if (!\array_key_exists($getByPathOperationName, $operations)) { + // Do not remove operation if it does not exist + return $webResponseResource; + } + + unset($operations[$getByPathOperationName]); + $webResponseResource['resources'][$this->webResponseClass]['operations'] = array_filter($operations); + return $webResponseResource; } protected function getCollectionOperations(NodeTypeInterface $nodeType): array { + $operations = []; if ($nodeType->isReachable()) { $groups = [ "nodes_sources_base", @@ -98,23 +279,60 @@ protected function getCollectionOperations(NodeTypeInterface $nodeType): array "document_display_sources", ...$this->getGroupedFieldsSerializationGroups($nodeType) ]; - return [ - 'get' => [ - 'method' => 'GET', - 'normalization_context' => [ - 'enable_max_depth' => true, - 'groups' => array_values(array_filter(array_unique($groups))) - ], + + $collectionOperationName = $this->apiResourceOperationNameGenerator->generate( + $nodeType->getSourceEntityFullQualifiedClassName(), + 'get_collection' + ); + $operations = array_merge( + $operations, + [ + $collectionOperationName => [ + 'method' => 'GET', + 'class' => GetCollection::class, + 'shortName' => $nodeType->getName(), + 'normalizationContext' => [ + 'enable_max_depth' => true, + 'groups' => array_values(array_filter(array_unique($groups))) + ], + ] ] - ]; + ); } - return []; + if ($nodeType->isPublishable()) { + $archivesOperationName = $this->apiResourceOperationNameGenerator->generate( + $nodeType->getSourceEntityFullQualifiedClassName(), + 'archives_collection' + ); + $operations = array_merge( + $operations, + [ + $archivesOperationName => [ + 'method' => 'GET', + 'class' => GetCollection::class, + 'shortName' => $nodeType->getName(), + 'uriTemplate' => $this->getResourceUriPrefix($nodeType) . '/archives', + 'extraProperties' => [ + 'archive_enabled' => true, + ], + 'openapiContext' => [ + 'summary' => sprintf( + 'Retrieve all %s ressources archives months and years', + $nodeType->getName() + ), + ], + ] + ] + ); + } + return $operations; } - protected function getItemOperations(NodeTypeInterface $nodeType): array + protected function getItemOperationSerializationGroups(NodeTypeInterface $nodeType): array { - $groups = [ + return [ "nodes_sources", + "node_listing", "urls", "tag_base", "translation_base", @@ -123,40 +341,25 @@ protected function getItemOperations(NodeTypeInterface $nodeType): array "document_display_sources", ...$this->getGroupedFieldsSerializationGroups($nodeType) ]; - $operations = [ - 'get' => [ + } + + protected function getItemOperations(NodeTypeInterface $nodeType): array + { + $groups = $this->getItemOperationSerializationGroups($nodeType); + $itemOperationName = $this->apiResourceOperationNameGenerator->generate( + $nodeType->getSourceEntityFullQualifiedClassName(), + 'get' + ); + return [ + $itemOperationName => [ 'method' => 'GET', - 'normalization_context' => [ + 'class' => Get::class, + 'shortName' => $nodeType->getName(), + 'normalizationContext' => [ 'groups' => array_values(array_filter(array_unique($groups))) ], ] ]; - - /* - * Create itemOperation for WebResponseController action - */ - if ($nodeType->isReachable()) { - $operations['getByPath'] = [ - 'method' => 'GET', - 'path' => '/web_response_by_path', - 'read' => false, - 'controller' => 'RZ\Roadiz\CoreBundle\Api\Controller\GetWebResponseByPathController', - 'pagination_enabled' => false, - 'normalization_context' => [ - 'enable_max_depth' => true, - 'groups' => array_merge(array_values(array_filter(array_unique($groups))), [ - 'web_response', - 'walker', - 'walker_level', - 'walker_metadata', - 'meta', - 'children', - ]) - ], - ]; - } - - return $operations; } protected function getGroupedFieldsSerializationGroups(NodeTypeInterface $nodeType): array diff --git a/src/NodeType/ApiResourceOperationNameGenerator.php b/src/NodeType/ApiResourceOperationNameGenerator.php new file mode 100644 index 00000000..bd3da5fa --- /dev/null +++ b/src/NodeType/ApiResourceOperationNameGenerator.php @@ -0,0 +1,28 @@ +afterLast('\\') + ->trimPrefix('NS') + ->lower() + ->toString(), + $operation + ); + } + + public function generateGetByPath(string $resourceClass): string + { + return self::generate($resourceClass, 'get_by_path'); + } +} diff --git a/src/NodeType/DefaultValuesResolver.php b/src/NodeType/DefaultValuesResolver.php index cc2fc94d..e2180403 100644 --- a/src/NodeType/DefaultValuesResolver.php +++ b/src/NodeType/DefaultValuesResolver.php @@ -23,7 +23,6 @@ public function __construct( $this->inheritanceType = $inheritanceType; } - public function getDefaultValuesAmongAllFields(NodeTypeFieldInterface $field): array { /* @@ -31,7 +30,7 @@ public function getDefaultValuesAmongAllFields(NodeTypeFieldInterface $field): a * SQL field won't be shared between all node types. */ if ($this->inheritanceType === Configuration::INHERITANCE_TYPE_JOINED) { - return array_map('trim', explode(',', $field->getDefaultValues())); + return array_map('trim', explode(',', $field->getDefaultValues() ?? '')); } else { /* * With single table inheritance, we need to get all default values @@ -43,7 +42,7 @@ public function getDefaultValuesAmongAllFields(NodeTypeFieldInterface $field): a 'type' => $field->getType(), ]); foreach ($nodeTypeFields as $nodeTypeField) { - $defaultValues = array_merge($defaultValues, array_map('trim', explode(',', $nodeTypeField->getDefaultValues()))); + $defaultValues = array_merge($defaultValues, array_map('trim', explode(',', $nodeTypeField->getDefaultValues() ?? ''))); } return $defaultValues; } diff --git a/src/Preview/EventSubscriber/PreviewBarSubscriber.php b/src/Preview/EventSubscriber/PreviewBarSubscriber.php index f1e05262..a3ab4354 100644 --- a/src/Preview/EventSubscriber/PreviewBarSubscriber.php +++ b/src/Preview/EventSubscriber/PreviewBarSubscriber.php @@ -10,16 +10,10 @@ use Symfony\Component\HttpKernel\Event\ResponseEvent; use Symfony\Component\HttpKernel\KernelEvents; -class PreviewBarSubscriber implements EventSubscriberInterface +final class PreviewBarSubscriber implements EventSubscriberInterface { - protected PreviewResolverInterface $previewResolver; - - /** - * @param PreviewResolverInterface $previewResolver - */ - public function __construct(PreviewResolverInterface $previewResolver) + public function __construct(private readonly PreviewResolverInterface $previewResolver) { - $this->previewResolver = $previewResolver; } /** diff --git a/src/Preview/EventSubscriber/PreviewModeSubscriber.php b/src/Preview/EventSubscriber/PreviewModeSubscriber.php index 2850b83f..82540443 100644 --- a/src/Preview/EventSubscriber/PreviewModeSubscriber.php +++ b/src/Preview/EventSubscriber/PreviewModeSubscriber.php @@ -6,31 +6,21 @@ use RZ\Roadiz\CoreBundle\Preview\Exception\PreviewNotAllowedException; use RZ\Roadiz\CoreBundle\Preview\PreviewResolverInterface; +use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpKernel\Event\ControllerEvent; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\Event\ResponseEvent; use Symfony\Component\HttpKernel\KernelEvents; -use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; -use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; -use Symfony\Component\Security\Core\Security; -class PreviewModeSubscriber implements EventSubscriberInterface +final class PreviewModeSubscriber implements EventSubscriberInterface { public const QUERY_PARAM_NAME = '_preview'; - protected PreviewResolverInterface $previewResolver; - protected TokenStorageInterface $tokenStorage; - protected Security $security; - public function __construct( - PreviewResolverInterface $previewResolver, - TokenStorageInterface $tokenStorage, - Security $security + private readonly PreviewResolverInterface $previewResolver, + private readonly Security $security ) { - $this->previewResolver = $previewResolver; - $this->tokenStorage = $tokenStorage; - $this->security = $security; } /** @@ -39,9 +29,10 @@ public function __construct( public static function getSubscribedEvents(): array { return [ - KernelEvents::REQUEST => ['onKernelRequest', 9999], + KernelEvents::REQUEST => ['onKernelRequest', 2047], KernelEvents::CONTROLLER => ['onControllerMatched', 10], - KernelEvents::RESPONSE => 'onResponse', + // Must Triggered after API platform AddHeadersListener + KernelEvents::RESPONSE => ['onResponse', -255], ]; } @@ -61,9 +52,9 @@ public function onKernelRequest(RequestEvent $event): void $request = $event->getRequest(); if ( $event->isMainRequest() && - $request->query->has(static::QUERY_PARAM_NAME) && + $request->query->has(self::QUERY_PARAM_NAME) && \in_array( - $request->query->get(static::QUERY_PARAM_NAME, 0), + $request->query->get(self::QUERY_PARAM_NAME, 0), ['true', true, '1', 1, 'on', 'yes', 'y'], true ) @@ -83,11 +74,6 @@ public function onKernelRequest(RequestEvent $event): void public function onControllerMatched(ControllerEvent $event): void { if ($this->supports() && $event->isMainRequest()) { - /** @var TokenInterface|null $token */ - $token = $this->tokenStorage->getToken(); - if (null === $token || !$token->isAuthenticated()) { - throw new PreviewNotAllowedException('You are not authenticated to use preview mode.'); - } if (!$this->security->isGranted($this->previewResolver->getRequiredRole())) { throw new PreviewNotAllowedException('You are not granted to use preview mode.'); } @@ -103,10 +89,11 @@ public function onResponse(ResponseEvent $event): void { if ($this->supports()) { $response = $event->getResponse(); - $response->expire(); + $response->setMaxAge(0); + $response->setSharedMaxAge(0); $response->headers->addCacheControlDirective('no-store'); $response->headers->add(['X-Roadiz-Preview' => true]); - $event->setResponse($response); + $response->setPrivate(); } } } diff --git a/src/Preview/Exception/PreviewNotAllowedException.php b/src/Preview/Exception/PreviewNotAllowedException.php index 2c376595..07da1c7b 100644 --- a/src/Preview/Exception/PreviewNotAllowedException.php +++ b/src/Preview/Exception/PreviewNotAllowedException.php @@ -12,7 +12,7 @@ */ class PreviewNotAllowedException extends AccessDeniedHttpException { - public function __construct($message = "You are not allowed to use preview mode.") + public function __construct(string $message = "You are not allowed to use preview mode.") { parent::__construct($message); } diff --git a/src/Preview/RequestPreviewRevolver.php b/src/Preview/RequestPreviewRevolver.php index 632f9889..b11944fb 100644 --- a/src/Preview/RequestPreviewRevolver.php +++ b/src/Preview/RequestPreviewRevolver.php @@ -33,7 +33,7 @@ public function isPreview(): bool if (null === $request) { return false; } - return $request->attributes->get('preview', false); + return $request->attributes->getBoolean('preview'); } public function getRequiredRole(): string diff --git a/src/Preview/User/PreviewUserProvider.php b/src/Preview/User/PreviewUserProvider.php index cea9ee39..70e07fb8 100644 --- a/src/Preview/User/PreviewUserProvider.php +++ b/src/Preview/User/PreviewUserProvider.php @@ -6,7 +6,7 @@ use RZ\Roadiz\CoreBundle\Preview\PreviewResolverInterface; use Symfony\Component\Security\Core\Exception\AccessDeniedException; -use Symfony\Component\Security\Core\Security; +use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\Security\Core\User\UserInterface; final class PreviewUserProvider implements PreviewUserProviderInterface diff --git a/src/Realm/RealmResolver.php b/src/Realm/RealmResolver.php index fa4f0bf2..d900db6b 100644 --- a/src/Realm/RealmResolver.php +++ b/src/Realm/RealmResolver.php @@ -5,22 +5,22 @@ namespace RZ\Roadiz\CoreBundle\Realm; use Doctrine\Persistence\ManagerRegistry; +use Psr\Cache\CacheItemPoolInterface; use RZ\Roadiz\CoreBundle\Entity\Node; use RZ\Roadiz\CoreBundle\Entity\Realm; use RZ\Roadiz\CoreBundle\Model\RealmInterface; use RZ\Roadiz\CoreBundle\Security\Authorization\Voter\RealmVoter; use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException; -use Symfony\Component\Security\Core\Security; +use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\String\Slugger\AsciiSlugger; final class RealmResolver implements RealmResolverInterface { - private ManagerRegistry $managerRegistry; - private Security $security; - - public function __construct(ManagerRegistry $managerRegistry, Security $security) - { - $this->managerRegistry = $managerRegistry; - $this->security = $security; + public function __construct( + private readonly ManagerRegistry $managerRegistry, + private readonly Security $security, + private readonly CacheItemPoolInterface $cache + ) { } public function getRealms(?Node $node): array @@ -31,6 +31,14 @@ public function getRealms(?Node $node): array return $this->managerRegistry->getRepository(Realm::class)->findByNode($node); } + public function getRealmsWithSerializationGroup(?Node $node): array + { + if (null === $node) { + return []; + } + return $this->managerRegistry->getRepository(Realm::class)->findByNodeWithSerializationGroup($node); + } + public function isGranted(RealmInterface $realm): bool { return $this->security->isGranted(RealmVoter::READ, $realm); @@ -45,4 +53,59 @@ public function denyUnlessGranted(RealmInterface $realm): void ); } } + + private function getUserCacheKey(): string + { + return (new AsciiSlugger()) + ->slug($this->security->getUser()?->getUserIdentifier() ?? 'anonymous') + ->__toString(); + } + + public function getGrantedRealms(): array + { + $cacheItem = $this->cache->getItem('granted_realms_' . $this->getUserCacheKey()); + if (!$cacheItem->isHit()) { + $allRealms = $this->managerRegistry->getRepository(Realm::class)->findBy([]); + $cacheItem->set(array_filter($allRealms, fn(RealmInterface $realm) => $this->isGranted($realm))); + $cacheItem->expiresAfter(new \DateInterval('PT1H')); + $this->cache->save($cacheItem); + } + return $cacheItem->get(); + } + + public function getDeniedRealms(): array + { + $cacheItem = $this->cache->getItem('denied_realms_' . $this->getUserCacheKey()); + if (!$cacheItem->isHit()) { + $allRealms = $this->managerRegistry->getRepository(Realm::class)->findBy([]); + $cacheItem->set(array_filter($allRealms, fn(RealmInterface $realm) => !$this->isGranted($realm))); + $cacheItem->expiresAfter(new \DateInterval('PT1H')); + $this->cache->save($cacheItem); + } + return $cacheItem->get(); + } + + public function hasRealms(): bool + { + $cacheItem = $this->cache->getItem('app_has_realms'); + if (!$cacheItem->isHit()) { + $hasRealms = $this->managerRegistry->getRepository(Realm::class)->countBy([]) > 0; + $cacheItem->set($hasRealms); + $cacheItem->expiresAfter(new \DateInterval('PT2H')); + $this->cache->save($cacheItem); + } + return $cacheItem->get(); + } + + public function hasRealmsWithSerializationGroup(): bool + { + $cacheItem = $this->cache->getItem('app_has_realms_with_serialization_group'); + if (!$cacheItem->isHit()) { + $hasRealms = $this->managerRegistry->getRepository(Realm::class)->countWithSerializationGroup() > 0; + $cacheItem->set($hasRealms); + $cacheItem->expiresAfter(new \DateInterval('PT2H')); + $this->cache->save($cacheItem); + } + return $cacheItem->get(); + } } diff --git a/src/Realm/RealmResolverInterface.php b/src/Realm/RealmResolverInterface.php index 970b094f..0124db98 100644 --- a/src/Realm/RealmResolverInterface.php +++ b/src/Realm/RealmResolverInterface.php @@ -10,11 +10,24 @@ interface RealmResolverInterface { + /** + * @return bool Does current application has realms? + */ + public function hasRealms(): bool; + /** + * @return bool Does current application has realms with serialization groups? + */ + public function hasRealmsWithSerializationGroup(): bool; /** * @param Node|null $node * @return RealmInterface[] */ public function getRealms(?Node $node): array; + /** + * @param Node|null $node + * @return RealmInterface[] + */ + public function getRealmsWithSerializationGroup(?Node $node): array; public function isGranted(RealmInterface $realm): bool; /** @@ -23,4 +36,14 @@ public function isGranted(RealmInterface $realm): bool; * @throws UnauthorizedHttpException */ public function denyUnlessGranted(RealmInterface $realm): void; + + /** + * @return RealmInterface[] Return all realms granted to current user. + */ + public function getGrantedRealms(): array; + + /** + * @return RealmInterface[] Return all realms denied from current user. + */ + public function getDeniedRealms(): array; } diff --git a/src/Repository/AttributeValueRepository.php b/src/Repository/AttributeValueRepository.php index 1f4fe5e4..d494a49c 100644 --- a/src/Repository/AttributeValueRepository.php +++ b/src/Repository/AttributeValueRepository.php @@ -24,14 +24,15 @@ public function __construct( /** * @param AttributableInterface $attributable - * - * @return array + * @param bool $orderByWeight + * @return array */ public function findByAttributable( - AttributableInterface $attributable + AttributableInterface $attributable, + bool $orderByWeight = false ): array { $qb = $this->createQueryBuilder('av'); - return $qb->addSelect('avt') + $qb = $qb->addSelect('avt') ->addSelect('a') ->addSelect('at') ->addSelect('ad') @@ -44,12 +45,18 @@ public function findByAttributable( ->leftJoin('a.group', 'ag') ->leftJoin('ag.attributeGroupTranslations', 'agt') ->andWhere($qb->expr()->eq('av.node', ':attributable')) - ->addOrderBy('av.position', 'ASC') ->setParameters([ 'attributable' => $attributable, ]) - ->setCacheable(true) - ->getQuery() + ->setCacheable(true); + + if ($orderByWeight) { + $qb->addOrderBy('a.weight', 'DESC'); + } else { + $qb->addOrderBy('av.position', 'ASC'); + } + + return $qb->getQuery() ->getResult(); } diff --git a/src/Repository/CustomFormAnswerRepository.php b/src/Repository/CustomFormAnswerRepository.php index 3810a296..d7774a73 100644 --- a/src/Repository/CustomFormAnswerRepository.php +++ b/src/Repository/CustomFormAnswerRepository.php @@ -53,6 +53,6 @@ public function deleteByCustomFormSubmittedBefore(CustomForm $customForm, \DateT ->delete() ->setParameter(':customForm', $customForm) ->setParameter(':submittedAt', $submittedAt); - return $qb->getQuery()->getSingleScalarResult(); + return (int) $qb->getQuery()->getSingleScalarResult(); } } diff --git a/src/Repository/CustomFormRepository.php b/src/Repository/CustomFormRepository.php index 027ea52e..62df6b92 100644 --- a/src/Repository/CustomFormRepository.php +++ b/src/Repository/CustomFormRepository.php @@ -33,14 +33,38 @@ public function findAllWithRetentionTime(): array ->getResult(); } + /** + * @param Node $node + * @param NodeTypeFieldInterface $field + * @return CustomForm[] + * @deprecated Use findByNodeAndFieldName instead + */ public function findByNodeAndField(Node $node, NodeTypeFieldInterface $field): array { $query = $this->_em->createQuery(' SELECT cf FROM RZ\Roadiz\CoreBundle\Entity\CustomForm cf INNER JOIN cf.nodes ncf - WHERE ncf.field = :field AND ncf.node = :node + WHERE ncf.fieldName = :fieldName AND ncf.node = :node + ORDER BY ncf.position ASC') + ->setParameter('fieldName', $field->getName()) + ->setParameter('node', $node); + + return $query->getResult(); + } + + /** + * @param Node $node + * @param string $fieldName + * @return CustomForm[] + */ + public function findByNodeAndFieldName(Node $node, string $fieldName): array + { + $query = $this->_em->createQuery(' + SELECT cf FROM RZ\Roadiz\CoreBundle\Entity\CustomForm cf + INNER JOIN cf.nodes ncf + WHERE ncf.fieldName = :fieldName AND ncf.node = :node ORDER BY ncf.position ASC') - ->setParameter('field', $field) + ->setParameter('fieldName', $fieldName) ->setParameter('node', $node); return $query->getResult(); diff --git a/src/Repository/DocumentRepository.php b/src/Repository/DocumentRepository.php index a135e1e6..163c8268 100644 --- a/src/Repository/DocumentRepository.php +++ b/src/Repository/DocumentRepository.php @@ -528,7 +528,8 @@ public function countBy( /** * @param NodesSources $nodeSource * @param NodeTypeFieldInterface $field - * @return array + * @return array + * @deprecated Use findByNodeSourceAndFieldName instead */ public function findByNodeSourceAndField( NodesSources $nodeSource, @@ -538,10 +539,35 @@ public function findByNodeSourceAndField( $qb->addSelect('dt') ->leftJoin('d.documentTranslations', 'dt', 'WITH', 'dt.translation = :translation') ->innerJoin('d.nodesSourcesByFields', 'nsf', 'WITH', 'nsf.nodeSource = :nodeSource') - ->andWhere($qb->expr()->eq('nsf.field', ':field')) + ->andWhere($qb->expr()->eq('nsf.fieldName', ':fieldName')) + ->andWhere($qb->expr()->eq('d.raw', ':raw')) + ->addOrderBy('nsf.position', 'ASC') + ->setParameter('fieldName', $field->getName()) + ->setParameter('nodeSource', $nodeSource) + ->setParameter('translation', $nodeSource->getTranslation()) + ->setParameter('raw', false) + ->setCacheable(true); + + return $qb->getQuery()->getResult(); + } + + /** + * @param NodesSources $nodeSource + * @param string $fieldName + * @return array + */ + public function findByNodeSourceAndFieldName( + NodesSources $nodeSource, + string $fieldName + ): array { + $qb = $this->createQueryBuilder('d'); + $qb->addSelect('dt') + ->leftJoin('d.documentTranslations', 'dt', 'WITH', 'dt.translation = :translation') + ->innerJoin('d.nodesSourcesByFields', 'nsf', 'WITH', 'nsf.nodeSource = :nodeSource') + ->andWhere($qb->expr()->eq('nsf.fieldName', ':fieldName')) ->andWhere($qb->expr()->eq('d.raw', ':raw')) ->addOrderBy('nsf.position', 'ASC') - ->setParameter('field', $field) + ->setParameter('fieldName', $fieldName) ->setParameter('nodeSource', $nodeSource) ->setParameter('translation', $nodeSource->getTranslation()) ->setParameter('raw', false) diff --git a/src/Repository/EntityRepository.php b/src/Repository/EntityRepository.php index ddf6f510..1b0e0223 100644 --- a/src/Repository/EntityRepository.php +++ b/src/Repository/EntityRepository.php @@ -24,6 +24,7 @@ use RZ\Roadiz\CoreBundle\Doctrine\Event\QueryEvent; use RZ\Roadiz\CoreBundle\Doctrine\ORM\SimpleQueryBuilder; use RZ\Roadiz\CoreBundle\Entity\Tag; +use Symfony\Contracts\EventDispatcher\Event; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; /** @@ -90,6 +91,7 @@ public function __construct(ManagerRegistry $registry, string $entityClass, Even */ protected function dispatchQueryBuilderEvent(QueryBuilder $qb, string $entityClass): void { + // @phpstan-ignore-next-line $this->dispatcher->dispatch(new QueryBuilderSelectEvent($qb, $entityClass)); } @@ -98,10 +100,11 @@ protected function dispatchQueryBuilderEvent(QueryBuilder $qb, string $entityCla * @param string $property * @param mixed $value * - * @return QueryBuilderBuildEvent + * @return Event */ protected function dispatchQueryBuilderBuildEvent(QueryBuilder $qb, string $property, mixed $value): object { + // @phpstan-ignore-next-line return $this->dispatcher->dispatch(new QueryBuilderBuildEvent( $qb, $this->getEntityName(), @@ -114,10 +117,11 @@ protected function dispatchQueryBuilderBuildEvent(QueryBuilder $qb, string $prop /** * @param Query $query * - * @return QueryEvent + * @return Event */ protected function dispatchQueryEvent(Query $query): object { + // @phpstan-ignore-next-line return $this->dispatcher->dispatch(new QueryEvent( $query, $this->getEntityName() @@ -129,10 +133,11 @@ protected function dispatchQueryEvent(Query $query): object * @param string $property * @param mixed $value * - * @return QueryBuilderApplyEvent + * @return Event */ protected function dispatchQueryBuilderApplyEvent(QueryBuilder $qb, string $property, mixed $value): object { + // @phpstan-ignore-next-line return $this->dispatcher->dispatch(new QueryBuilderApplyEvent( $qb, $this->getEntityName(), @@ -257,7 +262,6 @@ public static function getSearchableColumnsNames(ClassMetadataInfo $metadata): a 'childrenOrder', 'childrenOrderDirection', 'password', - 'salt', 'token', 'confirmationToken' ]) @@ -341,12 +345,13 @@ public function searchBy( // Add ordering foreach ($orders as $key => $value) { if ( - str_contains($key, static::NODE_ALIAS . '.') && + (\str_starts_with($key, 'node.') || \str_starts_with($key, static::NODE_ALIAS . '.')) && $this->hasJoinedNode($qb, $alias) ) { + $key = preg_replace('#^node\.#', static::NODE_ALIAS . '.', $key); $qb->addOrderBy($key, $value); } elseif ( - str_contains($key, static::NODESSOURCES_ALIAS . '.') && + \str_starts_with($key, static::NODESSOURCES_ALIAS . '.') && $this->hasJoinedNodesSources($qb, $alias) ) { $qb->addOrderBy($key, $value); @@ -490,7 +495,7 @@ protected function applyFilterByTag(array &$criteria, QueryBuilder $qb): void * * @param QueryBuilder $qb * @param string $alias - * @return boolean + * @return bool */ protected function hasJoinedNode(QueryBuilder $qb, string $alias) { @@ -502,7 +507,7 @@ protected function hasJoinedNode(QueryBuilder $qb, string $alias) * * @param QueryBuilder $qb * @param string $alias - * @return boolean + * @return bool */ protected function hasJoinedNodesSources(QueryBuilder $qb, string $alias) { @@ -514,7 +519,7 @@ protected function hasJoinedNodesSources(QueryBuilder $qb, string $alias) * * @param QueryBuilder $qb * @param string $alias - * @return boolean + * @return bool */ protected function hasJoinedNodeType(QueryBuilder $qb, string $alias) { diff --git a/src/Repository/LogRepository.php b/src/Repository/LogRepository.php index ac633d1f..3419bca8 100644 --- a/src/Repository/LogRepository.php +++ b/src/Repository/LogRepository.php @@ -5,9 +5,12 @@ namespace RZ\Roadiz\CoreBundle\Repository; use Doctrine\ORM\Query; +use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\Tools\Pagination\Paginator; use Doctrine\Persistence\ManagerRegistry; -use RZ\Roadiz\CoreBundle\Entity\Log; +use RZ\Roadiz\CoreBundle\Entity\Node; +use RZ\Roadiz\CoreBundle\Entity\NodesSources; +use RZ\Roadiz\CoreBundle\Logger\Entity\Log; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; /** @@ -30,30 +33,16 @@ public function __construct( */ public function findLatestByNodesSources(int $maxResult = 5): Paginator { - /* - * We need to split this query in 2 for performance matter. - * - * SELECT l1_.id, l1_.datetime, n0_.id - * FROM log AS l1_ - * INNER JOIN nodes_sources n0_ ON l1_.node_source_id = n0_.id - * WHERE l1_.id IN ( - * SELECT MAX(id) - * FROM log - * GROUP BY node_source_id - * ) - * ORDER BY l1_.datetime DESC - * LIMIT 8 - */ - $subQb = $this->createQueryBuilder('slog'); $subQb->select($subQb->expr()->max('slog.id')) - ->addGroupBy('slog.nodeSource'); + ->andWhere($subQb->expr()->in('slog.entityClass', ':entityClass')) + ->addGroupBy('slog.entityId'); $qb = $this->createQueryBuilder('log'); $qb->select('log.id as id') - ->innerJoin('log.nodeSource', 'ns') ->andWhere($qb->expr()->in('log.id', $subQb->getQuery()->getDQL())) ->orderBy('log.datetime', 'DESC') + ->setParameter(':entityClass', [NodesSources::class, Node::class]) ->setMaxResults($maxResult) ; $ids = $qb->getQuery() @@ -61,11 +50,7 @@ public function findLatestByNodesSources(int $maxResult = 5): Paginator ->getScalarResult(); $qb2 = $this->createQueryBuilder('log'); - $qb2->addSelect('ns, n, dbf') - ->andWhere($qb2->expr()->in('log.id', ':id')) - ->innerJoin('log.nodeSource', 'ns') - ->leftJoin('ns.documentsByFields', 'dbf') - ->innerJoin('ns.node', 'n') + $qb2->andWhere($qb2->expr()->in('log.id', ':id')) ->orderBy('log.datetime', 'DESC') ->setParameter(':id', array_map(function (array $item) { return $item['id']; @@ -73,4 +58,33 @@ public function findLatestByNodesSources(int $maxResult = 5): Paginator return new Paginator($qb2->getQuery(), true); } + + public function findByNode(Node $node): array + { + $qb = $this->getAllRelatedToNodeQueryBuilder($node); + return $qb->getQuery()->getResult(); + } + + public function getAllRelatedToNodeQueryBuilder(Node $node): QueryBuilder + { + $qb = $this->createQueryBuilder('obj'); + $qb->andWhere($qb->expr()->orX( + $qb->expr()->andX( + $qb->expr()->eq('obj.entityClass', ':nodeClass'), + $qb->expr()->in('obj.entityId', ':nodeId') + ), + $qb->expr()->andX( + $qb->expr()->eq('obj.entityClass', ':nodeSourceClass'), + $qb->expr()->in('obj.entityId', ':nodeSourceId') + ), + )); + $qb->addOrderBy('obj.datetime', 'DESC'); + $qb->setParameter('nodeClass', Node::class); + $qb->setParameter('nodeSourceClass', NodesSources::class); + $qb->setParameter('nodeId', [$node->getId()]); + $qb->setParameter('nodeSourceId', $node->getNodeSources()->map(function (NodesSources $ns) { + return $ns->getId(); + })->toArray()); + return $qb; + } } diff --git a/src/Repository/LoginAttemptRepository.php b/src/Repository/LoginAttemptRepository.php deleted file mode 100644 index 377cb35c..00000000 --- a/src/Repository/LoginAttemptRepository.php +++ /dev/null @@ -1,139 +0,0 @@ - - */ -final class LoginAttemptRepository extends EntityRepository -{ - public function __construct( - ManagerRegistry $registry, - EventDispatcherInterface $dispatcher - ) { - parent::__construct($registry, LoginAttempt::class, $dispatcher); - } - - /** - * @param string $username - * - * @return bool - * @throws \Doctrine\ORM\NoResultException - * @throws \Doctrine\ORM\NonUniqueResultException - */ - public function isUsernameBlocked(string $username): bool - { - $qb = $this->createQueryBuilder('la'); - return $qb->select('COUNT(la)') - ->andWhere($qb->expr()->gte('la.blocksLoginUntil', ':now')) - ->andWhere($qb->expr()->eq('la.username', ':username')) - ->getQuery() - ->setParameters([ - 'now' => new \DateTime('now'), - 'username' => $username, - ]) - ->getSingleScalarResult() > 0 - ; - } - - /** - * Checks if an IP address tries more than 10 usernames - * in the last 5 minutes. - * - * @param string $ipAddress - * @param int $seconds - * @param int $count - * - * @return bool - * @throws \Doctrine\ORM\NoResultException - * @throws \Doctrine\ORM\NonUniqueResultException - */ - public function isIpAddressBlocked(string $ipAddress, int $seconds = 1200, int $count = 10): bool - { - $qb = $this->createQueryBuilder('la'); - $query = $qb->select('SUM(la.attemptCount)') - ->andWhere($qb->expr()->gte('la.date', ':now')) - ->andWhere($qb->expr()->eq('la.ipAddress', ':ipAddress')) - ->getQuery() - ->setParameters([ - 'now' => (new \DateTime())->sub(new \DateInterval('PT' . $seconds . 'S')), - 'ipAddress' => $ipAddress, - ]) - ; - return $query->getSingleScalarResult() > $count ? true : false; - } - - /** - * @param string $ipAddress - * @param string $username - * - * @return LoginAttempt - * @throws \Doctrine\ORM\ORMException - */ - public function findOrCreateOneByIpAddressAndUsername(string $ipAddress, string $username): LoginAttempt - { - /** @var LoginAttempt|null $loginAttempt */ - $loginAttempt = $this->findOneBy([ - 'ipAddress' => $ipAddress, - 'username' => $username, - ]); - if (null === $loginAttempt) { - $loginAttempt = new LoginAttempt($ipAddress, $username); - $this->_em->persist($loginAttempt); - } - - return $loginAttempt; - } - - /** - * @param string $ipAddress - * @param string $username - */ - public function resetLoginAttempts(string $ipAddress, string $username): void - { - $qb = $this->_em->createQueryBuilder(); - $qb->delete(LoginAttempt::class, 'la') - ->andWhere($qb->expr()->eq('la.ipAddress', ':ipAddress')) - ->andWhere($qb->expr()->eq('la.username', ':username')) - ->getQuery() - ->execute([ - 'username' => $username, - 'ipAddress' => $ipAddress, - ]) - ; - } - - /** - * @param string $ipAddress - */ - public function purgeLoginAttempts(string $ipAddress): void - { - $qb = $this->_em->createQueryBuilder(); - $qb->delete(LoginAttempt::class, 'la') - ->andWhere($qb->expr()->eq('la.ipAddress', ':ipAddress')) - ->getQuery() - ->execute([ - 'ipAddress' => $ipAddress, - ]) - ; - } - - public function cleanLoginAttempts(): void - { - $qb = $this->_em->createQueryBuilder(); - $qb->delete(LoginAttempt::class, 'la') - ->andWhere($qb->expr()->lte('la.blocksLoginUntil', ':date')) - ->getQuery() - ->execute([ - 'date' => (new \DateTime())->sub(new \DateInterval('P1D')), - ]) - ; - } -} diff --git a/src/Repository/NodeRepository.php b/src/Repository/NodeRepository.php index 33dfcfa2..2d298d0b 100644 --- a/src/Repository/NodeRepository.php +++ b/src/Repository/NodeRepository.php @@ -20,10 +20,10 @@ use RZ\Roadiz\CoreBundle\Doctrine\ORM\SimpleQueryBuilder; use RZ\Roadiz\CoreBundle\Entity\Node; use RZ\Roadiz\CoreBundle\Entity\NodesSources; -use RZ\Roadiz\CoreBundle\Entity\NodeTypeField; -use RZ\Roadiz\CoreBundle\Entity\UrlAlias; use RZ\Roadiz\CoreBundle\Preview\PreviewResolverInterface; -use Symfony\Component\Security\Core\Security; +use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\String\Slugger\AsciiSlugger; +use Symfony\Contracts\EventDispatcher\Event; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; /** @@ -45,10 +45,11 @@ public function __construct( * @param string $property * @param mixed $value * - * @return object|QueryBuilderBuildEvent + * @return Event */ protected function dispatchQueryBuilderBuildEvent(QueryBuilder $qb, string $property, mixed $value): object { + // @phpstan-ignore-next-line return $this->dispatcher->dispatch( new QueryBuilderBuildEvent($qb, Node::class, $property, $value, $this->getEntityName()) ); @@ -59,10 +60,11 @@ protected function dispatchQueryBuilderBuildEvent(QueryBuilder $qb, string $prop * @param string $property * @param mixed $value * - * @return object|QueryBuilderApplyEvent + * @return Event */ protected function dispatchQueryBuilderApplyEvent(QueryBuilder $qb, string $property, mixed $value): object { + // @phpstan-ignore-next-line return $this->dispatcher->dispatch( new QueryBuilderApplyEvent($qb, Node::class, $property, $value, $this->getEntityName()) ); @@ -328,6 +330,7 @@ public function findBy( $this->applyFilterByTag($criteria, $qb); $this->applyFilterByCriteria($criteria, $qb); $this->applyTranslationByTag($qb, $translation); + // @phpstan-ignore-next-line $query = $qb->getQuery(); $this->dispatchQueryEvent($query); @@ -451,6 +454,7 @@ public function findOneBy( $this->applyFilterByTag($criteria, $qb); $this->applyFilterByCriteria($criteria, $qb); $this->applyTranslationByTag($qb, $translation); + // @phpstan-ignore-next-line $query = $qb->getQuery(); $this->dispatchQueryEvent($query); @@ -805,14 +809,14 @@ public function findByNodeAndField( $qb = $this->createQueryBuilder(self::NODE_ALIAS); $qb->select(self::NODE_ALIAS) ->innerJoin('n.aNodes', 'ntn') - ->andWhere($qb->expr()->eq('ntn.field', ':field')) + ->andWhere($qb->expr()->eq('ntn.fieldName', ':fieldName')) ->andWhere($qb->expr()->eq('ntn.nodeA', ':nodeA')) ->addOrderBy('ntn.position', 'ASC') ->setCacheable(true); $this->alterQueryBuilderWithAuthorizationChecker($qb); - $qb->setParameter('field', $field) + $qb->setParameter('fieldName', $field->getName()) ->setParameter('nodeA', $node); return $qb->getQuery()->getResult(); @@ -833,7 +837,7 @@ public function findByNodeAndFieldAndTranslation( $qb->select('n, ns') ->innerJoin('n.aNodes', 'ntn') ->innerJoin('n.nodeSources', self::NODESSOURCES_ALIAS) - ->andWhere($qb->expr()->eq('ntn.field', ':field')) + ->andWhere($qb->expr()->eq('ntn.fieldName', ':fieldName')) ->andWhere($qb->expr()->eq('ntn.nodeA', ':nodeA')) ->andWhere($qb->expr()->eq('ns.translation', ':translation')) ->addOrderBy('ntn.position', 'ASC') @@ -841,7 +845,7 @@ public function findByNodeAndFieldAndTranslation( $this->alterQueryBuilderWithAuthorizationChecker($qb); - $qb->setParameter('field', $field) + $qb->setParameter('fieldName', $field->getName()) ->setParameter('nodeA', $node) ->setParameter('translation', $translation); @@ -860,14 +864,14 @@ public function findByReverseNodeAndField( $qb = $this->createQueryBuilder(self::NODE_ALIAS); $qb->select(self::NODE_ALIAS) ->innerJoin('n.bNodes', 'ntn') - ->andWhere($qb->expr()->eq('ntn.field', ':field')) + ->andWhere($qb->expr()->eq('ntn.fieldName', ':fieldName')) ->andWhere($qb->expr()->eq('ntn.nodeB', ':nodeB')) ->addOrderBy('ntn.position', 'ASC') ->setCacheable(true); $this->alterQueryBuilderWithAuthorizationChecker($qb); - $qb->setParameter('field', $field) + $qb->setParameter('fieldName', $field->getName()) ->setParameter('nodeB', $node); return $qb->getQuery()->getResult(); @@ -888,7 +892,7 @@ public function findByReverseNodeAndFieldAndTranslation( $qb->select('n, ns') ->innerJoin('n.bNodes', 'ntn') ->innerJoin('n.nodeSources', self::NODESSOURCES_ALIAS) - ->andWhere($qb->expr()->eq('ntn.field', ':field')) + ->andWhere($qb->expr()->eq('ntn.fieldName', ':fieldName')) ->andWhere($qb->expr()->eq('ns.translation', ':translation')) ->andWhere($qb->expr()->eq('ntn.nodeB', ':nodeB')) ->addOrderBy('ntn.position', 'ASC') @@ -896,7 +900,7 @@ public function findByReverseNodeAndFieldAndTranslation( $this->alterQueryBuilderWithAuthorizationChecker($qb); - $qb->setParameter('field', $field) + $qb->setParameter('fieldName', $field->getName()) ->setParameter('translation', $translation) ->setParameter('nodeB', $node); @@ -905,15 +909,15 @@ public function findByReverseNodeAndFieldAndTranslation( /** * @param Node $node - * @return array + * @return array */ - public function findAllOffspringIdByNode(Node $node) + public function findAllOffspringIdByNode(Node $node): array { - $theOffprings = []; - $in = [$node->getId()]; + $theOffsprings = []; + $in = \array_filter([(int) $node->getId()]); do { - $theOffprings = array_merge($theOffprings, $in); + $theOffsprings = array_merge($theOffsprings, $in); $subQb = $this->createQueryBuilder('n'); $subQb->select('n.id') ->andWhere($subQb->expr()->in('n.parent', ':tab')) @@ -927,7 +931,7 @@ public function findAllOffspringIdByNode(Node $node) $in[] = (int) $item['id']; } } while (!empty($in)); - return $theOffprings; + return $theOffsprings; } /** @@ -1107,4 +1111,122 @@ public function findLatestPositionInParent(Node $parent = null): int return (int) $qb->getQuery()->getSingleScalarResult(); } + + /** + * Use by UniqueEntity Validator to bypass node status query filtering. + * + * @param array $criteria + * @return Node|null + * @throws NonUniqueResultException + */ + public function findOneWithoutSecurity(array $criteria): ?Node + { + $this->setDisplayingAllNodesStatuses(true); + if (count($criteria) === 1 && !empty($criteria['nodeName'])) { + /* + * Test if nodeName is used as an url-alias too + */ + $nodeName = (new AsciiSlugger())->slug($criteria['nodeName'])->lower()->trim()->toString(); + + $qb = $this->createQueryBuilder('o'); + $qb->leftJoin('o.nodeSources', 'ns') + ->leftJoin('ns.urlAliases', 'ua') + ->andWhere($qb->expr()->orX( + $qb->expr()->eq('ua.alias', ':nodeName'), + $qb->expr()->eq('o.nodeName', ':nodeName') + )) + ->setParameter('nodeName', $nodeName) + ->setMaxResults(1) + ->setCacheable(true); + ; + return $qb->getQuery()->getOneOrNullResult(); + } + return $this->findOneBy($criteria); + } + + protected function classicLikeComparison( + string $pattern, + QueryBuilder $qb, + string $alias = EntityRepository::DEFAULT_ALIAS + ): QueryBuilder { + $qb = parent::classicLikeComparison($pattern, $qb, $alias); + $qb + ->leftJoin($alias . '.attributeValues', 'av') + ->leftJoin('av.attributeValueTranslations', 'avt') + ; + $value = '%' . strip_tags(\mb_strtolower($pattern)) . '%'; + $qb->orWhere($qb->expr()->like('LOWER(avt.value)', $qb->expr()->literal($value))); + $qb->orWhere($qb->expr()->like('LOWER(' . $alias . '.nodeName)', $qb->expr()->literal($value))); + return $qb; + } + + /** + * Get previous node from hierarchy + */ + public function findPreviousNode( + Node $node, + ?array $criteria = null, + ?array $order = null + ): ?Node { + if ($node->getPosition() <= 1) { + return null; + } + if (null === $order) { + $order = []; + } + + if (null === $criteria) { + $criteria = []; + } + + $criteria['parent'] = $node->getParent(); + /* + * Use < operator to get first previous nodeSource + * even if it’s not the previous position index + */ + $criteria['position'] = [ + '<', + $node->getPosition(), + ]; + + $order['position'] = 'DESC'; + + return $this->findOneBy( + $criteria, + $order + ); + } + + /** + * Get next node from hierarchy. + */ + public function findNextNode( + Node $node, + ?array $criteria = null, + ?array $order = null + ): ?Node { + if (null === $criteria) { + $criteria = []; + } + if (null === $order) { + $order = []; + } + + $criteria['parent'] = $node->getParent(); + + /* + * Use > operator to get first next nodeSource + * even if it’s not the next position index + */ + $criteria['position'] = [ + '>', + $node->getPosition(), + ]; + $order['position'] = 'ASC'; + + return $this->findOneBy( + $criteria, + $order + ); + } } diff --git a/src/Repository/NodesCustomFormsRepository.php b/src/Repository/NodesCustomFormsRepository.php index dc41cec0..112e0e90 100644 --- a/src/Repository/NodesCustomFormsRepository.php +++ b/src/Repository/NodesCustomFormsRepository.php @@ -5,6 +5,7 @@ namespace RZ\Roadiz\CoreBundle\Repository; use Doctrine\Persistence\ManagerRegistry; +use RZ\Roadiz\Contracts\NodeType\NodeTypeFieldInterface; use RZ\Roadiz\CoreBundle\Entity\Node; use RZ\Roadiz\CoreBundle\Entity\NodesCustomForms; use RZ\Roadiz\CoreBundle\Entity\NodeTypeField; @@ -22,20 +23,38 @@ public function __construct( ) { parent::__construct($registry, NodesCustomForms::class, $dispatcher); } + /** - * @param Node $node - * @param NodeTypeField $field - * - * @return integer + * @param Node $node + * @param NodeTypeFieldInterface $field + * @return int + * @throws \Doctrine\ORM\NoResultException + * @throws \Doctrine\ORM\NonUniqueResultException + * @deprecated Use getLatestPositionForFieldName instead */ - public function getLatestPosition(Node $node, NodeTypeField $field) + public function getLatestPosition(Node $node, NodeTypeFieldInterface $field): int + { + $query = $this->_em->createQuery(' + SELECT MAX(ncf.position) FROM RZ\Roadiz\CoreBundle\Entity\NodesCustomForms ncf + WHERE ncf.node = :node AND ncf.fieldName = :fieldName') + ->setParameter('node', $node) + ->setParameter('fieldName', $field->getName()); + + $latestPosition = $query->getSingleScalarResult(); + + return is_numeric($latestPosition) ? (int) $latestPosition : 0; + } + + public function getLatestPositionForFieldName(Node $node, string $fieldName): int { $query = $this->_em->createQuery(' SELECT MAX(ncf.position) FROM RZ\Roadiz\CoreBundle\Entity\NodesCustomForms ncf - WHERE ncf.node = :node AND ncf.field = :field') + WHERE ncf.node = :node AND ncf.fieldName = :fieldName') ->setParameter('node', $node) - ->setParameter('field', $field); + ->setParameter('fieldName', $fieldName); + + $latestPosition = $query->getSingleScalarResult(); - return (int) $query->getSingleScalarResult(); + return is_numeric($latestPosition) ? (int) $latestPosition : 0; } } diff --git a/src/Repository/NodesSourcesDocumentsRepository.php b/src/Repository/NodesSourcesDocumentsRepository.php index 0c24dc5d..3cc0ea1c 100644 --- a/src/Repository/NodesSourcesDocumentsRepository.php +++ b/src/Repository/NodesSourcesDocumentsRepository.php @@ -7,6 +7,7 @@ use Doctrine\ORM\NonUniqueResultException; use Doctrine\ORM\NoResultException; use Doctrine\Persistence\ManagerRegistry; +use RZ\Roadiz\Contracts\NodeType\NodeTypeFieldInterface; use RZ\Roadiz\CoreBundle\Entity\NodesSources; use RZ\Roadiz\CoreBundle\Entity\NodesSourcesDocuments; use RZ\Roadiz\CoreBundle\Entity\NodeTypeField; @@ -26,19 +27,35 @@ public function __construct( /** * @param NodesSources $nodeSource - * @param NodeTypeField $field + * @param NodeTypeFieldInterface $field * @return int * @throws NoResultException * @throws NonUniqueResultException + * @deprecated Use getLatestPositionForFieldName instead */ - public function getLatestPosition(NodesSources $nodeSource, NodeTypeField $field): int + public function getLatestPosition(NodesSources $nodeSource, NodeTypeFieldInterface $field): int { $query = $this->_em->createQuery(' SELECT MAX(nsd.position) FROM RZ\Roadiz\CoreBundle\Entity\NodesSourcesDocuments nsd - WHERE nsd.nodeSource = :nodeSource AND nsd.field = :field') + WHERE nsd.nodeSource = :nodeSource AND nsd.fieldName = :fieldName') ->setParameter('nodeSource', $nodeSource) - ->setParameter('field', $field); + ->setParameter('fieldName', $field->getName()); - return (int) $query->getSingleScalarResult(); + $latestPosition = $query->getSingleScalarResult(); + + return is_numeric($latestPosition) ? (int) $latestPosition : 0; + } + + public function getLatestPositionForFieldName(NodesSources $nodeSource, string $fieldName): int + { + $query = $this->_em->createQuery(' + SELECT MAX(nsd.position) FROM RZ\Roadiz\CoreBundle\Entity\NodesSourcesDocuments nsd + WHERE nsd.nodeSource = :nodeSource AND nsd.fieldName = :fieldName') + ->setParameter('nodeSource', $nodeSource) + ->setParameter('fieldName', $fieldName); + + $latestPosition = $query->getSingleScalarResult(); + + return is_numeric($latestPosition) ? (int) $latestPosition : 0; } } diff --git a/src/Repository/NodesSourcesRepository.php b/src/Repository/NodesSourcesRepository.php index dfe4b3d3..088a52cd 100644 --- a/src/Repository/NodesSourcesRepository.php +++ b/src/Repository/NodesSourcesRepository.php @@ -4,6 +4,7 @@ namespace RZ\Roadiz\CoreBundle\Repository; +use Doctrine\ORM\NonUniqueResultException; use Doctrine\ORM\Query; use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\Tools\Pagination\Paginator; @@ -14,24 +15,25 @@ use RZ\Roadiz\CoreBundle\Doctrine\Event\QueryBuilder\QueryBuilderNodesSourcesBuildEvent; use RZ\Roadiz\CoreBundle\Doctrine\Event\QueryNodesSourcesEvent; use RZ\Roadiz\CoreBundle\Doctrine\ORM\SimpleQueryBuilder; -use RZ\Roadiz\CoreBundle\Entity\Log; use RZ\Roadiz\CoreBundle\Entity\Node; use RZ\Roadiz\CoreBundle\Entity\NodesSources; -use RZ\Roadiz\CoreBundle\Entity\NodeTypeField; use RZ\Roadiz\CoreBundle\Exception\SolrServerNotAvailableException; +use RZ\Roadiz\CoreBundle\Logger\Entity\Log; use RZ\Roadiz\CoreBundle\Preview\PreviewResolverInterface; use RZ\Roadiz\CoreBundle\SearchEngine\NodeSourceSearchHandlerInterface; use RZ\Roadiz\CoreBundle\SearchEngine\SearchResultsInterface; +use RZ\Roadiz\CoreBundle\SearchEngine\SolrSearchResultItem; use RZ\Roadiz\CoreBundle\SearchEngine\SolrSearchResults; -use Symfony\Component\Security\Core\Security; +use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Contracts\EventDispatcher\Event; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; /** * EntityRepository that implements search engine query with Solr. * * @template T of NodesSources - * @extends StatusAwareRepository - * @template-extends StatusAwareRepository + * @extends StatusAwareRepository + * @template-extends StatusAwareRepository */ class NodesSourcesRepository extends StatusAwareRepository { @@ -60,10 +62,11 @@ public function __construct( * @param string $property * @param mixed $value * - * @return object|QueryBuilderNodesSourcesBuildEvent + * @return Event */ protected function dispatchQueryBuilderBuildEvent(QueryBuilder $qb, string $property, mixed $value): object { + // @phpstan-ignore-next-line return $this->dispatcher->dispatch( new QueryBuilderNodesSourcesBuildEvent($qb, $property, $value, $this->getEntityName()) ); @@ -74,10 +77,11 @@ protected function dispatchQueryBuilderBuildEvent(QueryBuilder $qb, string $prop * @param string $property * @param mixed $value * - * @return object|QueryBuilderNodesSourcesApplyEvent + * @return Event */ protected function dispatchQueryBuilderApplyEvent(QueryBuilder $qb, string $property, mixed $value): object { + // @phpstan-ignore-next-line return $this->dispatcher->dispatch( new QueryBuilderNodesSourcesApplyEvent($qb, $property, $value, $this->getEntityName()) ); @@ -86,10 +90,11 @@ protected function dispatchQueryBuilderApplyEvent(QueryBuilder $qb, string $prop /** * @param Query $query * - * @return object|QueryNodesSourcesEvent + * @return Event */ protected function dispatchQueryEvent(Query $query): object { + // @phpstan-ignore-next-line return $this->dispatcher->dispatch( new QueryNodesSourcesEvent($query, $this->getEntityName()) ); @@ -147,9 +152,10 @@ protected function filterByCriteria( if ($key == "tags" || $key == "tagExclusive") { continue; } - /* + /** * Main QueryBuilder dispatch loop for * custom properties criteria. + * @var QueryBuilderNodesSourcesBuildEvent $event */ $event = $this->dispatchQueryBuilderBuildEvent($qb, $key, $value); @@ -183,6 +189,7 @@ protected function applyFilterByCriteria(array &$criteria, QueryBuilder $qb): vo continue; } + /** @var QueryBuilderNodesSourcesApplyEvent $event */ $event = $this->dispatchQueryBuilderApplyEvent($qb, $key, $value); if (!$event->isPropagationStopped()) { $simpleQB->bindValue($key, $value); @@ -212,29 +219,17 @@ public function alterQueryBuilderWithAuthorizationChecker( * Forbid deleted node for backend user when authorizationChecker not null. */ if (!$this->hasJoinedNode($qb, $prefix)) { - $qb->innerJoin( - $prefix . '.node', - static::NODE_ALIAS, - 'WITH', - $qb->expr()->lte(static::NODE_ALIAS . '.status', Node::PUBLISHED) - ); - } else { - $qb->andWhere($qb->expr()->lte(static::NODE_ALIAS . '.status', Node::PUBLISHED)); + $qb->innerJoin($prefix . '.node', static::NODE_ALIAS); } + $qb->andWhere($qb->expr()->lte(static::NODE_ALIAS . '.status', Node::PUBLISHED)); } else { /* * Forbid unpublished node for anonymous and not backend users. */ if (!$this->hasJoinedNode($qb, $prefix)) { - $qb->innerJoin( - $prefix . '.node', - static::NODE_ALIAS, - 'WITH', - $qb->expr()->eq(static::NODE_ALIAS . '.status', Node::PUBLISHED) - ); - } else { - $qb->andWhere($qb->expr()->eq(static::NODE_ALIAS . '.status', Node::PUBLISHED)); + $qb->innerJoin($prefix . '.node', static::NODE_ALIAS); } + $qb->andWhere($qb->expr()->eq(static::NODE_ALIAS . '.status', Node::PUBLISHED)); } return $qb; } @@ -245,8 +240,8 @@ public function alterQueryBuilderWithAuthorizationChecker( * * @param array $criteria * @param array|null $orderBy - * @param integer|null $limit - * @param integer|null $offset + * @param int|null $limit + * @param int|null $offset * @return QueryBuilder */ protected function getContextualQuery( @@ -254,7 +249,7 @@ protected function getContextualQuery( array $orderBy = null, $limit = null, $offset = null - ) { + ): QueryBuilder { $qb = $this->createQueryBuilder(static::NODESSOURCES_ALIAS); $this->alterQueryBuilderWithAuthorizationChecker($qb, static::NODESSOURCES_ALIAS); $qb->addSelect(static::NODE_ALIAS); @@ -267,7 +262,7 @@ protected function getContextualQuery( // Add ordering if (null !== $orderBy) { foreach ($orderBy as $key => $value) { - if (false !== \mb_strpos($key, 'node.')) { + if (\str_contains($key, 'node.')) { $simpleKey = str_replace('node.', '', $key); $qb->addOrderBy(static::NODE_ALIAS . '.' . $simpleKey, $value); } else { @@ -357,7 +352,7 @@ public function countBy(mixed $criteria): int * @param array|null $orderBy * @param int|null $limit * @param int|null $offset - * @return array|Paginator + * @return array|Paginator */ public function findBy( array $criteria, @@ -412,7 +407,7 @@ public function findBy( public function findOneBy( array $criteria, array $orderBy = null - ) { + ): ?NodesSources { $qb = $this->getContextualQuery( $criteria, $orderBy, @@ -441,7 +436,7 @@ public function findOneBy( * * @param string $query Solr query string (for example: `text:Lorem Ipsum`) * @param int $limit Result number to fetch (default: all) - * @return array + * @return array> */ public function findBySearchQuery(string $query, int $limit = 25): array { @@ -521,7 +516,6 @@ public function findByTextQuery( ->andWhere($qb->expr()->orX( $qb->expr()->like(static::NODESSOURCES_ALIAS . '.title', ':query'), $qb->expr()->like(static::NODESSOURCES_ALIAS . '.metaTitle', ':query'), - $qb->expr()->like(static::NODESSOURCES_ALIAS . '.metaKeywords', ':query'), $qb->expr()->like(static::NODESSOURCES_ALIAS . '.metaDescription', ':query') )) ->orderBy(static::NODESSOURCES_ALIAS . '.title', 'ASC') @@ -563,18 +557,20 @@ public function findByTextQuery( * @param int $maxResult * @return Paginator */ - public function findByLatestUpdated($maxResult = 5) + public function findByLatestUpdated(int $maxResult = 5): Paginator { $subQuery = $this->_em->createQueryBuilder(); - $subQuery->select('sns.id') + $subQuery->select('slog.entityId') ->from(Log::class, 'slog') - ->innerJoin(NodesSources::class, 'sns') - ->andWhere($subQuery->expr()->isNotNull('slog.nodeSource')) + ->andWhere($subQuery->expr()->eq('slog.entityClass', ':entityClass')) ->orderBy('slog.datetime', 'DESC'); $query = $this->createQueryBuilder(static::NODESSOURCES_ALIAS); - $query->andWhere($query->expr()->in(static::NODESSOURCES_ALIAS . '.id', $subQuery->getQuery()->getDQL())); - $query->setMaxResults($maxResult); + $query + ->andWhere($query->expr()->in(static::NODESSOURCES_ALIAS . '.id', $subQuery->getQuery()->getDQL())) + ->setParameter(':entityClass', NodesSources::class) + ->setMaxResults($maxResult) + ; return new Paginator($query->getQuery()); } @@ -646,7 +642,7 @@ protected function prepareComparisons(array &$criteria, QueryBuilder $qb, $alias if (!$event->isPropagationStopped()) { $baseKey = $simpleQB->getParameterKey($key); - if (false !== \mb_strpos($key, 'node.nodeType.')) { + if (\str_contains($key, 'node.nodeType.')) { if (!$this->hasJoinedNode($qb, $alias)) { $qb->innerJoin($alias . '.node', static::NODE_ALIAS); } @@ -656,7 +652,7 @@ protected function prepareComparisons(array &$criteria, QueryBuilder $qb, $alias $prefix = static::NODETYPE_ALIAS . '.'; $simpleKey = str_replace('node.nodeType.', '', $key); $qb->andWhere($simpleQB->buildExpressionWithoutBinding($value, $prefix, $simpleKey, $baseKey)); - } elseif (false !== \mb_strpos($key, 'node.')) { + } elseif (\str_contains($key, 'node.')) { if (!$this->hasJoinedNode($qb, $alias)) { $qb->innerJoin($alias . '.node', static::NODE_ALIAS); } @@ -704,7 +700,7 @@ public function searchBy( /** * @param NodesSources $nodesSources * @param NodeTypeFieldInterface $field - * + * @deprecated Use findByNodesSourcesAndFieldNameAndTranslation instead * @return array|null */ public function findByNodesSourcesAndFieldAndTranslation( @@ -716,7 +712,37 @@ public function findByNodesSourcesAndFieldAndTranslation( ->innerJoin('ns.node', static::NODE_ALIAS) ->leftJoin('ns.urlAliases', 'ua') ->innerJoin('n.aNodes', 'ntn') - ->andWhere($qb->expr()->eq('ntn.field', ':field')) + ->andWhere($qb->expr()->eq('ntn.fieldName', ':fieldName')) + ->andWhere($qb->expr()->eq('ntn.nodeA', ':nodeA')) + ->andWhere($qb->expr()->eq('ns.translation', ':translation')) + ->addOrderBy('ntn.position', 'ASC') + ->setCacheable(true); + + $this->alterQueryBuilderWithAuthorizationChecker($qb); + + $qb->setParameter('fieldName', $field->getName()) + ->setParameter('nodeA', $nodesSources->getNode()) + ->setParameter('translation', $nodesSources->getTranslation()); + + return $qb->getQuery()->getResult(); + } + + /** + * @param NodesSources $nodesSources + * @param string $fieldName + * + * @return array|null + */ + public function findByNodesSourcesAndFieldNameAndTranslation( + NodesSources $nodesSources, + string $fieldName + ): ?array { + $qb = $this->createQueryBuilder(static::NODESSOURCES_ALIAS); + $qb->select('ns, n, ua') + ->innerJoin('ns.node', static::NODE_ALIAS) + ->leftJoin('ns.urlAliases', 'ua') + ->innerJoin('n.aNodes', 'ntn') + ->andWhere($qb->expr()->eq('ntn.fieldName', ':fieldName')) ->andWhere($qb->expr()->eq('ntn.nodeA', ':nodeA')) ->andWhere($qb->expr()->eq('ns.translation', ':translation')) ->addOrderBy('ntn.position', 'ASC') @@ -724,7 +750,7 @@ public function findByNodesSourcesAndFieldAndTranslation( $this->alterQueryBuilderWithAuthorizationChecker($qb); - $qb->setParameter('field', $field) + $qb->setParameter('fieldName', $fieldName) ->setParameter('nodeA', $nodesSources->getNode()) ->setParameter('translation', $nodesSources->getTranslation()); @@ -752,4 +778,319 @@ public function findByNode(Node $node): array return $qb->getQuery()->getResult(); } + + protected function classicLikeComparison( + string $pattern, + QueryBuilder $qb, + string $alias = EntityRepository::DEFAULT_ALIAS + ): QueryBuilder { + $qb = parent::classicLikeComparison($pattern, $qb, $alias); + $qb + ->innerJoin($alias . '.node', static::NODE_ALIAS) + ->leftJoin(static::NODE_ALIAS . '.attributeValues', 'av') + ->leftJoin('av.attributeValueTranslations', 'avt') + ; + $value = '%' . strip_tags(\mb_strtolower($pattern)) . '%'; + $qb->orWhere($qb->expr()->like('LOWER(avt.value)', $qb->expr()->literal($value))); + $qb->orWhere($qb->expr()->like('LOWER(' . static::NODE_ALIAS . '.nodeName)', $qb->expr()->literal($value))); + return $qb; + } + + /** + * Get every nodeSources parents from direct parent to farthest ancestor. + * + * @param NodesSources $nodeSource + * @param array|null $criteria + * @return array + * @throws \Doctrine\ORM\NonUniqueResultException + */ + public function findParents( + NodesSources $nodeSource, + ?array $criteria = null + ): array { + $parentsNodeSources = []; + + if (null === $criteria) { + $criteria = []; + } + + $parent = $nodeSource; + + while (null !== $parent) { + $criteria = array_merge( + $criteria, + [ + 'node' => $parent->getNode()->getParent(), + 'translation' => $nodeSource->getTranslation(), + ] + ); + $currentParent = $this->findOneBy( + $criteria, + [] + ); + + if (null !== $currentParent) { + $parentsNodeSources[] = $currentParent; + } + + $parent = $currentParent; + } + + return $parentsNodeSources; + } + + /** + * Get children nodes sources to lock with current translation. + * + * @param NodesSources $nodeSource + * @param array|null $criteria Additional criteria + * @param array|null $order Non default ordering + * + * @return Paginator|array + */ + public function findChildren( + NodesSources $nodeSource, + array $criteria = null, + array $order = null + ): Paginator|array { + $defaultCriteria = [ + 'node.parent' => $nodeSource->getNode(), + 'translation' => $nodeSource->getTranslation(), + ]; + + if (null !== $order) { + $defaultOrder = $order; + } else { + $defaultOrder = [ + 'node.position' => 'ASC', + ]; + } + + if (null !== $criteria) { + $defaultCriteria = array_merge($defaultCriteria, $criteria); + } + + return $this->findBy( + $defaultCriteria, + $defaultOrder + ); + } + + /** + * Get first node-source among current node-source children. + * + * @param NodesSources|null $nodeSource + * @param array|null $criteria + * @param array|null $order + * + * @return NodesSources|null + * @throws NonUniqueResultException + */ + public function findFirstChild( + ?NodesSources $nodeSource, + array $criteria = null, + array $order = null + ): ?NodesSources { + $defaultCriteria = [ + 'node.parent' => $nodeSource?->getNode() ?? null, + ]; + + if (null !== $nodeSource) { + $defaultCriteria['translation'] = $nodeSource->getTranslation(); + } + + if (null !== $order) { + $defaultOrder = $order; + } else { + $defaultOrder = [ + 'node.position' => 'ASC', + ]; + } + + if (null !== $criteria) { + $defaultCriteria = array_merge($defaultCriteria, $criteria); + } + + return $this->findOneBy( + $defaultCriteria, + $defaultOrder + ); + } + + /** + * Get last node-source among current node-source children. + * + * @param NodesSources|null $nodeSource + * @param array|null $criteria + * @param array|null $order + * + * @return NodesSources|null + * @throws NonUniqueResultException + */ + public function findLastChild( + ?NodesSources $nodeSource, + array $criteria = null, + array $order = null + ): ?NodesSources { + $defaultCriteria = [ + 'node.parent' => $nodeSource?->getNode() ?? null, + ]; + + if (null !== $nodeSource) { + $defaultCriteria['translation'] = $nodeSource->getTranslation(); + } + + if (null !== $order) { + $defaultOrder = $order; + } else { + $defaultOrder = [ + 'node.position' => 'DESC', + ]; + } + + if (null !== $criteria) { + $defaultCriteria = array_merge($defaultCriteria, $criteria); + } + + return $this->findOneBy( + $defaultCriteria, + $defaultOrder + ); + } + + /** + * Get first node-source in the same parent as current node-source. + * + * @param NodesSources $nodeSource + * @param array|null $criteria + * @param array|null $order + * + * @return NodesSources|null + * @throws NonUniqueResultException + */ + public function findFirstSibling( + NodesSources $nodeSource, + array $criteria = null, + array $order = null + ): ?NodesSources { + if (null !== $nodeSource->getParent()) { + return $this->findFirstChild($nodeSource->getParent(), $criteria, $order); + } + return $this->findFirstChild(null, $criteria, $order); + } + + /** + * Get last node-source in the same parent as current node-source. + * + * @param NodesSources $nodeSource + * @param array|null $criteria + * @param array|null $order + * + * @return NodesSources|null + * @throws NonUniqueResultException + */ + public function findLastSibling( + NodesSources $nodeSource, + array $criteria = null, + array $order = null + ): ?NodesSources { + if (null !== $nodeSource->getParent()) { + return $this->findLastChild($nodeSource->getParent(), $criteria, $order); + } + return $this->findLastChild(null, $criteria, $order); + } + + /** + * Get previous node-source from hierarchy. + * + * @param NodesSources $nodeSource + * @param array|null $criteria + * @param array|null $order + * + * @return NodesSources|null + * @throws NonUniqueResultException + */ + public function findPrevious( + NodesSources $nodeSource, + array $criteria = null, + array $order = null + ): ?NodesSources { + if ($nodeSource->getNode()->getPosition() <= 1) { + return null; + } + + $defaultCriteria = [ + /* + * Use < operator to get first next nodeSource + * even if it’s not the next position index + */ + 'node.position' => [ + '<', + $nodeSource + ->getNode() + ->getPosition(), + ], + 'node.parent' => $nodeSource->getNode()->getParent(), + 'translation' => $nodeSource->getTranslation(), + ]; + if (null !== $criteria) { + $defaultCriteria = array_merge($defaultCriteria, $criteria); + } + + if (null === $order) { + $order = []; + } + + $order['node.position'] = 'DESC'; + + return $this->findOneBy( + $defaultCriteria, + $order + ); + } + + /** + * Get next node-source from hierarchy. + * + * @param NodesSources $nodeSource + * @param array|null $criteria + * @param array|null $order + * + * @return NodesSources|null + * @throws NonUniqueResultException + */ + public function findNext( + NodesSources $nodeSource, + array $criteria = null, + array $order = null + ): ?NodesSources { + $defaultCriteria = [ + /* + * Use > operator to get first next nodeSource + * even if it’s not the next position index + */ + 'node.position' => [ + '>', + $nodeSource + ->getNode() + ->getPosition(), + ], + 'node.parent' => $nodeSource->getNode()->getParent(), + 'translation' => $nodeSource->getTranslation(), + ]; + if (null !== $criteria) { + $defaultCriteria = array_merge($defaultCriteria, $criteria); + } + + if (null === $order) { + $order = []; + } + + $order['node.position'] = 'ASC'; + + return $this->findOneBy( + $defaultCriteria, + $order + ); + } } diff --git a/src/Repository/NodesToNodesRepository.php b/src/Repository/NodesToNodesRepository.php index a64042a3..d23079d9 100644 --- a/src/Repository/NodesToNodesRepository.php +++ b/src/Repository/NodesToNodesRepository.php @@ -7,6 +7,7 @@ use Doctrine\ORM\NonUniqueResultException; use Doctrine\ORM\NoResultException; use Doctrine\Persistence\ManagerRegistry; +use RZ\Roadiz\Contracts\NodeType\NodeTypeFieldInterface; use RZ\Roadiz\CoreBundle\Entity\Node; use RZ\Roadiz\CoreBundle\Entity\NodesToNodes; use RZ\Roadiz\CoreBundle\Entity\NodeTypeField; @@ -27,20 +28,35 @@ public function __construct( /** * @param Node $node - * @param NodeTypeField $field - * - * @return integer + * @param NodeTypeFieldInterface $field + * @return int * @throws NoResultException * @throws NonUniqueResultException + * @deprecated Use getLatestPositionForFieldName instead */ - public function getLatestPosition(Node $node, NodeTypeField $field): int + public function getLatestPosition(Node $node, NodeTypeFieldInterface $field): int { $query = $this->_em->createQuery(' SELECT MAX(ntn.position) FROM RZ\Roadiz\CoreBundle\Entity\NodesToNodes ntn - WHERE ntn.nodeA = :nodeA AND ntn.field = :field') + WHERE ntn.nodeA = :nodeA AND ntn.fieldName = :fieldName') ->setParameter('nodeA', $node) - ->setParameter('field', $field); + ->setParameter('fieldName', $field->getName()); - return (int) $query->getSingleScalarResult(); + $latestPosition = $query->getSingleScalarResult(); + + return is_numeric($latestPosition) ? (int) $latestPosition : 0; + } + + public function getLatestPositionForFieldName(Node $node, string $fieldName): int + { + $query = $this->_em->createQuery(' + SELECT MAX(ntn.position) FROM RZ\Roadiz\CoreBundle\Entity\NodesToNodes ntn + WHERE ntn.nodeA = :nodeA AND ntn.fieldName = :fieldName') + ->setParameter('nodeA', $node) + ->setParameter('fieldName', $fieldName); + + $latestPosition = $query->getSingleScalarResult(); + + return is_numeric($latestPosition) ? (int) $latestPosition : 0; } } diff --git a/src/Repository/PrefixAwareRepository.php b/src/Repository/PrefixAwareRepository.php index 4754a8ab..8db2f7bc 100644 --- a/src/Repository/PrefixAwareRepository.php +++ b/src/Repository/PrefixAwareRepository.php @@ -321,9 +321,11 @@ protected function classicLikeComparison( } foreach ($criteriaFields as $key => $value) { - $realKey = $this->getRealKey($qb, $key); - $fullKey = sprintf('LOWER(%s)', $realKey['prefix'] . $realKey['key']); - $qb->orWhere($qb->expr()->like($fullKey, $qb->expr()->literal($value))); + if (\is_string($key)) { + $realKey = $this->getRealKey($qb, $key); + $fullKey = sprintf('LOWER(%s)', $realKey['prefix'] . $realKey['key']); + $qb->orWhere($qb->expr()->like($fullKey, $qb->expr()->literal($value))); + } } return $qb; } diff --git a/src/Repository/RealmNodeRepository.php b/src/Repository/RealmNodeRepository.php index 0e68b682..4af3e76f 100644 --- a/src/Repository/RealmNodeRepository.php +++ b/src/Repository/RealmNodeRepository.php @@ -17,7 +17,7 @@ */ final class RealmNodeRepository extends EntityRepository { - public function findByNodeIdsAndRealmId(array $nodeIds, int $realmId): array + public function findByNodeIdsAndRealmId(array $nodeIds, int|string $realmId): array { $nodeIds = array_filter($nodeIds); if (empty($nodeIds)) { diff --git a/src/Repository/RealmRepository.php b/src/Repository/RealmRepository.php index 21b4c507..25382cba 100644 --- a/src/Repository/RealmRepository.php +++ b/src/Repository/RealmRepository.php @@ -41,4 +41,13 @@ public function findByNodeAndBehaviour(Node $node, string $realmBehaviour): arra return $qb->getQuery()->getResult(); } + + public function countWithSerializationGroup(): int + { + $qb = $this->createQueryBuilder('r'); + $qb->select($qb->expr()->count('r')) + ->andWhere($qb->expr()->isNotNull('r.serializationGroup')); + + return intval($qb->getQuery()->getSingleScalarResult()); + } } diff --git a/src/Repository/StatusAwareRepository.php b/src/Repository/StatusAwareRepository.php index 87fed3d5..8aaf6ede 100644 --- a/src/Repository/StatusAwareRepository.php +++ b/src/Repository/StatusAwareRepository.php @@ -8,7 +8,7 @@ use Doctrine\Persistence\ManagerRegistry; use RZ\Roadiz\CoreBundle\Entity\Node; use RZ\Roadiz\CoreBundle\Preview\PreviewResolverInterface; -use Symfony\Component\Security\Core\Security; +use Symfony\Bundle\SecurityBundle\Security; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; /** @@ -24,7 +24,7 @@ abstract class StatusAwareRepository extends EntityRepository /** * @param ManagerRegistry $registry - * @param string $entityClass + * @param class-string $entityClass * @param PreviewResolverInterface $previewResolver * @param EventDispatcherInterface $dispatcher * @param Security $security diff --git a/src/Repository/TagRepository.php b/src/Repository/TagRepository.php index 6f8c1f17..6d0eef31 100644 --- a/src/Repository/TagRepository.php +++ b/src/Repository/TagRepository.php @@ -13,6 +13,7 @@ use RZ\Roadiz\Core\AbstractEntities\TranslationInterface; use RZ\Roadiz\CoreBundle\Doctrine\ORM\SimpleQueryBuilder; use RZ\Roadiz\CoreBundle\Entity\Node; +use RZ\Roadiz\CoreBundle\Entity\NodesSources; use RZ\Roadiz\CoreBundle\Entity\Tag; use RZ\Roadiz\CoreBundle\Entity\TagTranslation; use RZ\Roadiz\CoreBundle\Entity\Translation; @@ -141,7 +142,7 @@ protected function filterByTranslation( $qb->leftJoin('tg.translatedTags', 'tt'); $qb->leftJoin( 'tt.translation', - static::TRANSLATION_ALIAS, + self::TRANSLATION_ALIAS, 'WITH', 't.defaultTranslation = true' ); @@ -233,7 +234,7 @@ protected function getCountContextualQueryWithTranslation( * @param integer|null $offset * @param TranslationInterface|null $translation * - * @return array|Paginator + * @return array|Paginator */ public function findBy( array $criteria, @@ -254,6 +255,7 @@ public function findBy( $this->applyFilterByNodes($criteria, $qb); $this->applyFilterByCriteria($criteria, $qb); $this->applyTranslationByTag($qb, $translation); + // @phpstan-ignore-next-line $query = $qb->getQuery(); $this->dispatchQueryEvent($query); @@ -298,6 +300,7 @@ public function findOneBy( $this->applyFilterByNodes($criteria, $qb); $this->applyFilterByCriteria($criteria, $qb); $this->applyTranslationByTag($qb, $translation); + // @phpstan-ignore-next-line $query = $qb->getQuery(); $this->dispatchQueryEvent($query); @@ -608,19 +611,19 @@ protected function prepareComparisons(array &$criteria, QueryBuilder $qb, $alias // Dots are forbidden in field definitions $baseKey = $simpleQB->getParameterKey($key); - if (false !== \mb_strpos($key, 'translation.')) { + if (\str_contains($key, 'translation.')) { /* * Search in translation fields */ $prefix = static::TRANSLATION_ALIAS . '.'; $key = str_replace('translation.', '', $key); - } elseif (false !== \mb_strpos($key, 'nodes.')) { + } elseif (\str_contains($key, 'nodes.')) { /* * Search in node fields */ $prefix = static::NODE_ALIAS . '.'; $key = str_replace('nodes.', '', $key); - } elseif (false !== \mb_strpos($key, 'translatedTag.')) { + } elseif (\str_contains($key, 'translatedTag.')) { /* * Search in translatedTags fields */ @@ -718,7 +721,7 @@ public function findOrCreateByPath(string $tagPath, ?TranslationInterface $trans * * @return Tag|null */ - public function findByPath(string $tagPath) + public function findByPath(string $tagPath): ?Tag { $tagPath = trim($tagPath); $tags = explode('/', $tagPath); @@ -743,10 +746,12 @@ public function findByPath(string $tagPath) * * Parent can be null for tag root * - * @param Tag|null $parent + * @param Tag|null $parent * @return int + * @throws NoResultException + * @throws NonUniqueResultException */ - public function findLatestPositionInParent(Tag $parent = null) + public function findLatestPositionInParent(Tag $parent = null): int { $qb = $this->createQueryBuilder('t'); $qb->select($qb->expr()->max('t.position')); @@ -760,4 +765,15 @@ public function findLatestPositionInParent(Tag $parent = null) return (int) $qb->getQuery()->getSingleScalarResult(); } + + public function findByNodesSources(NodesSources $nodesSources): array|Paginator + { + // @phpstan-ignore-next-line + return $this->findBy([ + "nodes" => $nodesSources->getNode(), + "translation" => $nodesSources->getTranslation(), + ], [ + 'position' => 'ASC', + ]); + } } diff --git a/src/Repository/UrlAliasRepository.php b/src/Repository/UrlAliasRepository.php index f9728c08..86815976 100644 --- a/src/Repository/UrlAliasRepository.php +++ b/src/Repository/UrlAliasRepository.php @@ -4,6 +4,8 @@ namespace RZ\Roadiz\CoreBundle\Repository; +use Doctrine\ORM\NonUniqueResultException; +use Doctrine\ORM\NoResultException; use Doctrine\Persistence\ManagerRegistry; use RZ\Roadiz\CoreBundle\Entity\UrlAlias; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; @@ -28,18 +30,21 @@ public function __construct( /** * Get all url aliases linked to given node. * - * @param integer $nodeId + * @param int|string|null $nodeId * - * @return array + * @return iterable */ - public function findAllFromNode($nodeId) + public function findAllFromNode(int|string|null $nodeId): iterable { + if (null === $nodeId) { + return []; + } $query = $this->_em->createQuery(' SELECT ua FROM RZ\Roadiz\CoreBundle\Entity\UrlAlias ua INNER JOIN ua.nodeSource ns INNER JOIN ns.node n WHERE n.id = :nodeId') - ->setParameter('nodeId', (int) $nodeId); + ->setParameter('nodeId', $nodeId); return $query->getResult(); } @@ -48,14 +53,16 @@ public function findAllFromNode($nodeId) * @param string $alias * * @return boolean + * @throws NoResultException + * @throws NonUniqueResultException */ - public function exists($alias) + public function exists(string $alias): bool { $query = $this->_em->createQuery(' SELECT COUNT(ua.alias) FROM RZ\Roadiz\CoreBundle\Entity\UrlAlias ua WHERE ua.alias = :alias') ->setParameter('alias', $alias); - return (bool) $query->getSingleScalarResult(); + return $query->getSingleScalarResult() > 0; } } diff --git a/src/Repository/UserLogEntryRepository.php b/src/Repository/UserLogEntryRepository.php index 44caf056..a3a3a171 100644 --- a/src/Repository/UserLogEntryRepository.php +++ b/src/Repository/UserLogEntryRepository.php @@ -18,7 +18,6 @@ * @method UserLogEntry|null findOneBy(array $criteria, array $orderBy = null) * @method UserLogEntry[] findAll() * @method UserLogEntry[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) - * @extends LogEntryRepository */ final class UserLogEntryRepository extends LogEntryRepository implements ServiceEntityRepositoryInterface { diff --git a/src/RoadizCoreBundle.php b/src/RoadizCoreBundle.php index 540b3975..a37d6c1c 100644 --- a/src/RoadizCoreBundle.php +++ b/src/RoadizCoreBundle.php @@ -14,6 +14,7 @@ use RZ\Roadiz\CoreBundle\DependencyInjection\Compiler\NodeWorkflowCompilerPass; use RZ\Roadiz\CoreBundle\DependencyInjection\Compiler\PathResolverCompilerPass; use RZ\Roadiz\CoreBundle\DependencyInjection\Compiler\RateLimitersCompilerPass; +use RZ\Roadiz\CoreBundle\DependencyInjection\Compiler\TreeWalkerDefinitionFactoryCompilerPass; use RZ\Roadiz\CoreBundle\DependencyInjection\Compiler\TwigLoaderCompilerPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\Bundle; @@ -40,5 +41,6 @@ public function build(ContainerBuilder $container): void $container->addCompilerPass(new PathResolverCompilerPass()); $container->addCompilerPass(new FlysystemStorageCompilerPass()); $container->addCompilerPass(new TwigLoaderCompilerPass()); + $container->addCompilerPass(new TreeWalkerDefinitionFactoryCompilerPass()); } } diff --git a/src/Routing/DeferredRouteCollection.php b/src/Routing/DeferredRouteCollection.php deleted file mode 100644 index 4597717c..00000000 --- a/src/Routing/DeferredRouteCollection.php +++ /dev/null @@ -1,29 +0,0 @@ -stopwatch = $stopwatch; - $this->logger = $logger ?? new NullLogger(); - $this->previewResolver = $previewResolver; } } diff --git a/src/Routing/InstallRouteCollection.php b/src/Routing/InstallRouteCollection.php deleted file mode 100644 index 7c86ff93..00000000 --- a/src/Routing/InstallRouteCollection.php +++ /dev/null @@ -1,36 +0,0 @@ -installClassname = $installClassname; - } - - /** - * {@inheritdoc} - */ - public function parseResources(): void - { - if (class_exists($this->installClassname)) { - $collection = call_user_func([$this->installClassname, 'getRoutes']); - if (null !== $collection) { - $this->addCollection($collection); - } - } else { - throw new \RuntimeException("Install class “" . $this->installClassname . "” does not exist.", 1); - } - } -} diff --git a/src/Routing/NodePathInfo.php b/src/Routing/NodePathInfo.php index 9ee32964..30e8710b 100644 --- a/src/Routing/NodePathInfo.php +++ b/src/Routing/NodePathInfo.php @@ -96,12 +96,16 @@ public function setContainsScheme(bool $containsScheme): NodePathInfo */ public function serialize(): string { - return \json_encode([ + $json = \json_encode([ 'path' => $this->getPath(), 'parameters' => $this->getParameters(), 'is_complete' => $this->isComplete(), 'contains_scheme' => $this->containsScheme() ]); + if (false === $json) { + throw new \RuntimeException('Unable to serialize NodePathInfo'); + } + return $json; } public function __serialize(): array diff --git a/src/Routing/NodeRouteHelper.php b/src/Routing/NodeRouteHelper.php index f51117d2..dfeec92a 100644 --- a/src/Routing/NodeRouteHelper.php +++ b/src/Routing/NodeRouteHelper.php @@ -19,11 +19,11 @@ final class NodeRouteHelper private LoggerInterface $logger; private string $defaultControllerNamespace; /** - * @var class-string + * @var class-string */ private string $defaultControllerClass; /** - * @var class-string|null + * @var class-string|null */ private ?string $controller = null; @@ -54,24 +54,35 @@ public function __construct( /** * Get controller class path for a given node. * - * @return string + * @return class-string|null */ - public function getController(): string + public function getController(): ?string { if (null === $this->controller) { - $namespace = $this->getControllerNamespace(); - $this->controller = $namespace . '\\' . + if (!$this->node->getNodeType()->isReachable()) { + return null; + } + $controllerClassName = $this->getControllerNamespace() . '\\' . StringHandler::classify($this->node->getNodeType()->getName()) . 'Controller'; - /* - * Use a default controller if no controller was found in Theme. - */ - if (!class_exists($this->controller) && $this->node->getNodeType()->isReachable()) { + if (\class_exists($controllerClassName)) { + $reflection = new \ReflectionClass($controllerClassName); + if (!$reflection->isSubclassOf(AbstractController::class)) { + throw new \InvalidArgumentException( + 'Controller class ' . $controllerClassName . ' must extends ' . AbstractController::class + ); + } + // @phpstan-ignore-next-line + $this->controller = $controllerClassName; + } else { + /* + * Use a default controller if no controller was found in Theme. + */ $this->controller = $this->defaultControllerClass; } } - + // @phpstan-ignore-next-line return $this->controller; } @@ -79,8 +90,8 @@ protected function getControllerNamespace(): string { $namespace = $this->defaultControllerNamespace; if (null !== $this->theme) { - $refl = new \ReflectionClass($this->theme->getClassName()); - $namespace = $refl->getNamespaceName() . '\\Controllers'; + $reflection = new \ReflectionClass($this->theme->getClassName()); + $namespace = $reflection->getNamespaceName() . '\\Controllers'; } return $namespace; } @@ -94,7 +105,6 @@ public function getMethod(): string * Return FALSE or TRUE if node is viewable. * * @return bool - * @throws \ReflectionException */ public function isViewable(): bool { @@ -109,33 +119,14 @@ public function isViewable(): bool ); return false; } - /* - * For archived and deleted nodes - */ - if ($this->node->getStatus() > Node::PUBLISHED) { - /* - * Not allowed to see deleted and archived nodes - * even for Admins - */ - return false; - } - /* - * For unpublished nodes - */ - if ($this->node->getStatus() < Node::PUBLISHED) { - if (true === $this->previewResolver->isPreview()) { - return true; - } - /* - * Not allowed to see unpublished nodes - */ - return false; + if ($this->previewResolver->isPreview()) { + return $this->node->isDraft() || $this->node->isPending() || $this->node->isPublished(); } /* * Everyone can view published nodes. */ - return true; + return $this->node->isPublished(); } } diff --git a/src/Routing/NodeRouter.php b/src/Routing/NodeRouter.php index bdea3b74..4d839b48 100644 --- a/src/Routing/NodeRouter.php +++ b/src/Routing/NodeRouter.php @@ -5,6 +5,7 @@ namespace RZ\Roadiz\CoreBundle\Routing; use Psr\Cache\CacheItemPoolInterface; +use Psr\Cache\InvalidArgumentException; use Psr\Log\LoggerInterface; use RZ\Roadiz\CoreBundle\Bag\Settings; use RZ\Roadiz\CoreBundle\Entity\NodesSources; @@ -15,7 +16,6 @@ use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\Routing\Exception\InvalidParameterException; use Symfony\Component\Routing\Exception\RouteNotFoundException; -use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Routing\Matcher\UrlMatcherInterface; use Symfony\Component\Routing\RequestContext; use Symfony\Component\Routing\RouteCollection; @@ -28,24 +28,12 @@ class NodeRouter extends Router implements VersatileGeneratorInterface */ public const NO_CACHE_PARAMETER = '_no_cache'; private ?Theme $theme = null; - private CacheItemPoolInterface $nodeSourceUrlCacheAdapter; - private Settings $settingsBag; - private EventDispatcherInterface $eventDispatcher; - /** - * @param NodeUrlMatcherInterface $matcher - * @param Settings $settingsBag - * @param EventDispatcherInterface $eventDispatcher - * @param CacheItemPoolInterface $nodeSourceUrlCacheAdapter - * @param array $options - * @param RequestContext $context - * @param LoggerInterface $logger - */ public function __construct( NodeUrlMatcherInterface $matcher, - Settings $settingsBag, - EventDispatcherInterface $eventDispatcher, - CacheItemPoolInterface $nodeSourceUrlCacheAdapter, + protected readonly Settings $settingsBag, + protected readonly EventDispatcherInterface $eventDispatcher, + protected readonly CacheItemPoolInterface $nodeSourceUrlCacheAdapter, RequestContext $context, LoggerInterface $logger, array $options = [] @@ -57,10 +45,7 @@ public function __construct( $context, $logger ); - $this->settingsBag = $settingsBag; - $this->eventDispatcher = $eventDispatcher; $this->matcher = $matcher; - $this->nodeSourceUrlCacheAdapter = $nodeSourceUrlCacheAdapter; } /** @@ -81,22 +66,6 @@ public function getMatcher(): UrlMatcherInterface return $this->matcher; } - /** - * No generator for a node router. - */ - public function getGenerator(): UrlGeneratorInterface - { - throw new \BadMethodCallException(get_class($this) . ' does not support path generation.'); - } - - /** - * @inheritDoc - */ - public function supports($name): bool - { - return ($name instanceof NodesSources || $name === RouteObjectInterface::OBJECT_BASED_ROUTE_NAME); - } - /** * @return Theme|null */ @@ -116,24 +85,11 @@ public function setTheme(?Theme $theme): NodeRouter } /** - * Convert a route identifier (name, content object etc) into a string - * usable for logging and other debug/error messages - * - * @param mixed $name - * @param array $parameters which should contain a content field containing - * a RouteReferrersReadInterface object - * - * @return string + * @inheritDoc */ - public function getRouteDebugMessage($name, array $parameters = []): string + public function getRouteDebugMessage(string $name, array $parameters = []): string { - if ($name instanceof NodesSources) { - @trigger_error('Passing an object as route name is deprecated since version 1.5. Pass the `RouteObjectInterface::OBJECT_BASED_ROUTE_NAME` as route name and the object in the parameters with key `RouteObjectInterface::ROUTE_OBJECT` resp the content id with content_id.', E_USER_DEPRECATED); - return '[' . $name->getTranslation()->getLocale() . ']' . - $name->getTitle() . ' - ' . - $name->getNode()->getNodeName() . - '[' . $name->getNode()->getId() . ']'; - } elseif (RouteObjectInterface::OBJECT_BASED_ROUTE_NAME === $name) { + if (RouteObjectInterface::OBJECT_BASED_ROUTE_NAME === $name) { if ( array_key_exists(RouteObjectInterface::ROUTE_OBJECT, $parameters) && $parameters[RouteObjectInterface::ROUTE_OBJECT] instanceof NodesSources @@ -150,19 +106,20 @@ public function getRouteDebugMessage($name, array $parameters = []): string /** * {@inheritdoc} + * @throws InvalidArgumentException */ public function generate(string $name, array $parameters = [], int $referenceType = self::ABSOLUTE_PATH): string { - if (RouteObjectInterface::OBJECT_BASED_ROUTE_NAME === $name) { - if ( - array_key_exists(RouteObjectInterface::ROUTE_OBJECT, $parameters) && - $parameters[RouteObjectInterface::ROUTE_OBJECT] instanceof NodesSources - ) { - $route = $parameters[RouteObjectInterface::ROUTE_OBJECT]; - unset($parameters[RouteObjectInterface::ROUTE_OBJECT]); - } else { - $route = null; - } + if (RouteObjectInterface::OBJECT_BASED_ROUTE_NAME !== $name) { + throw new RouteNotFoundException(); + } + + if ( + array_key_exists(RouteObjectInterface::ROUTE_OBJECT, $parameters) && + $parameters[RouteObjectInterface::ROUTE_OBJECT] instanceof NodesSources + ) { + $route = $parameters[RouteObjectInterface::ROUTE_OBJECT]; + unset($parameters[RouteObjectInterface::ROUTE_OBJECT]); } else { $route = null; } diff --git a/src/Routing/NodeUrlMatcher.php b/src/Routing/NodeUrlMatcher.php index 0ef42348..743e405f 100644 --- a/src/Routing/NodeUrlMatcher.php +++ b/src/Routing/NodeUrlMatcher.php @@ -8,22 +8,35 @@ use RZ\Roadiz\CoreBundle\Entity\NodesSources; use RZ\Roadiz\CoreBundle\Entity\Theme; use RZ\Roadiz\CoreBundle\Preview\PreviewResolverInterface; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Cmf\Component\Routing\RouteObjectInterface; use Symfony\Component\Routing\Exception\ResourceNotFoundException; use Symfony\Component\Routing\RequestContext; use Symfony\Component\Stopwatch\Stopwatch; /** - * UrlMatcher which tries to grab Node and Translation - * information for a route. + * UrlMatcher which tries to grab Node and Translation information for a route. */ final class NodeUrlMatcher extends DynamicUrlMatcher implements NodeUrlMatcherInterface { - protected PathResolverInterface $pathResolver; /** - * @var class-string + * @param PathResolverInterface $pathResolver + * @param RequestContext $context + * @param PreviewResolverInterface $previewResolver + * @param Stopwatch $stopwatch + * @param LoggerInterface $logger + * @param class-string $defaultControllerClass */ - private string $defaultControllerClass; + public function __construct( + private readonly PathResolverInterface $pathResolver, + RequestContext $context, + PreviewResolverInterface $previewResolver, + Stopwatch $stopwatch, + LoggerInterface $logger, + private readonly string $defaultControllerClass + ) { + parent::__construct($context, $previewResolver, $stopwatch, $logger); + } /** * @return array @@ -42,28 +55,7 @@ public function getDefaultSupportedFormatExtension(): string } /** - * @param PathResolverInterface $pathResolver - * @param RequestContext $context - * @param PreviewResolverInterface $previewResolver - * @param Stopwatch $stopwatch - * @param LoggerInterface $logger - * @param class-string $defaultControllerClass - */ - public function __construct( - PathResolverInterface $pathResolver, - RequestContext $context, - PreviewResolverInterface $previewResolver, - Stopwatch $stopwatch, - LoggerInterface $logger, - string $defaultControllerClass - ) { - parent::__construct($context, $previewResolver, $stopwatch, $logger); - $this->pathResolver = $pathResolver; - $this->defaultControllerClass = $defaultControllerClass; - } - - /** - * {@inheritdoc} + * @inheritDoc */ public function match(string $pathinfo): array { @@ -89,7 +81,6 @@ protected function getNodeRouteHelper(NodesSources $nodeSource, ?Theme $theme): * @param string $decodedUrl * @param Theme|null $theme * @return array - * @throws \ReflectionException */ public function matchNode(string $decodedUrl, ?Theme $theme): array { diff --git a/src/Routing/NodesSourcesPathResolver.php b/src/Routing/NodesSourcesPathResolver.php index 8fe3f0f5..2e8167c1 100644 --- a/src/Routing/NodesSourcesPathResolver.php +++ b/src/Routing/NodesSourcesPathResolver.php @@ -4,6 +4,7 @@ namespace RZ\Roadiz\CoreBundle\Routing; +use Doctrine\ORM\NonUniqueResultException; use Doctrine\Persistence\ManagerRegistry; use RZ\Roadiz\Core\AbstractEntities\TranslationInterface; use RZ\Roadiz\CoreBundle\Bag\Settings; @@ -19,28 +20,16 @@ final class NodesSourcesPathResolver implements PathResolverInterface { - private ManagerRegistry $managerRegistry; - private Stopwatch $stopwatch; private static string $nodeNamePattern = '[a-zA-Z0-9\-\_\.]+'; - private PreviewResolverInterface $previewResolver; - private Settings $settingsBag; - private RequestStack $requestStack; - private bool $useAcceptLanguageHeader; public function __construct( - ManagerRegistry $managerRegistry, - PreviewResolverInterface $previewResolver, - Stopwatch $stopwatch, - Settings $settingsBag, - RequestStack $requestStack, - bool $useAcceptLanguageHeader + private readonly ManagerRegistry $managerRegistry, + private readonly PreviewResolverInterface $previewResolver, + private readonly Stopwatch $stopwatch, + private readonly Settings $settingsBag, + private readonly RequestStack $requestStack, + private readonly bool $useAcceptLanguageHeader ) { - $this->stopwatch = $stopwatch; - $this->previewResolver = $previewResolver; - $this->managerRegistry = $managerRegistry; - $this->settingsBag = $settingsBag; - $this->requestStack = $requestStack; - $this->useAcceptLanguageHeader = $useAcceptLanguageHeader; } /** @@ -61,7 +50,7 @@ public function resolvePath( } if ($path === '/') { - $this->stopwatch->start('parseRootPath'); + $this->stopwatch->start('parseRootPath', 'routing'); $translation = $this->parseTranslation(); $nodeSource = $this->getHome($translation); $this->stopwatch->stop('parseRootPath'); @@ -96,13 +85,13 @@ public function resolvePath( } } - $this->stopwatch->start('parseTranslation'); + $this->stopwatch->start('parseTranslation', 'routing'); $translation = $this->parseTranslation($tokens); $this->stopwatch->stop('parseTranslation'); /* * Try with URL Aliases OR nodeName */ - $this->stopwatch->start('parseFromIdentifier'); + $this->stopwatch->start('parseFromIdentifier', 'routing'); $nodeSource = $this->parseFromIdentifier($tokens, $translation, $allowNonReachableNodes); $this->stopwatch->stop('parseFromIdentifier'); } @@ -156,6 +145,7 @@ private function getHome(TranslationInterface $translation): ?NodesSources * @param array $tokens * * @return TranslationInterface|null + * @throws NonUniqueResultException */ private function parseTranslation(array &$tokens = []): ?TranslationInterface { diff --git a/src/Routing/NodesSourcesUrlGenerator.php b/src/Routing/NodesSourcesUrlGenerator.php index 293d5e5e..943aa29e 100644 --- a/src/Routing/NodesSourcesUrlGenerator.php +++ b/src/Routing/NodesSourcesUrlGenerator.php @@ -13,31 +13,12 @@ */ final class NodesSourcesUrlGenerator { - protected ?Request $request; - protected ?NodesSources $nodeSource; - protected bool $forceLocale; - protected bool $forceLocaleWithUrlAlias; - protected NodesSourcesPathAggregator $pathAggregator; - - /** - * @param NodesSourcesPathAggregator $pathAggregator - * @param Request|null $request - * @param NodesSources|null $nodeSource - * @param bool $forceLocale - * @param bool $forceLocaleWithUrlAlias - */ public function __construct( - NodesSourcesPathAggregator $pathAggregator, - Request $request = null, - NodesSources $nodeSource = null, - bool $forceLocale = false, - bool $forceLocaleWithUrlAlias = false + private readonly NodesSourcesPathAggregator $pathAggregator, + private readonly ?NodesSources $nodeSource = null, + private readonly bool $forceLocale = false, + private readonly bool $forceLocaleWithUrlAlias = false ) { - $this->pathAggregator = $pathAggregator; - $this->request = $request; - $this->nodeSource = $nodeSource; - $this->forceLocale = $forceLocale; - $this->forceLocaleWithUrlAlias = $forceLocaleWithUrlAlias; } /** diff --git a/src/Routing/NullLoader.php b/src/Routing/NullLoader.php index 70eaacd3..673b621d 100644 --- a/src/Routing/NullLoader.php +++ b/src/Routing/NullLoader.php @@ -10,36 +10,23 @@ final class NullLoader implements LoaderInterface { /** - * Loads a resource. - * - * @param mixed $resource The resource - * @param string|null $type The resource type or null if unknown - * @return mixed - * - * @throws \Exception If something went wrong + * @inheritDoc */ - public function load($resource, $type = null): mixed + public function load(mixed $resource, string $type = null): mixed { return null; } /** - * Returns whether this class supports the given resource. - * - * @param mixed $resource A resource - * @param string|null $type The resource type or null if unknown - * - * @return bool True if this class supports the given resource, false otherwise + * @inheritDoc */ - public function supports($resource, $type = null): bool + public function supports(mixed $resource, string $type = null): bool { return true; } /** - * Gets the loader resolver. - * - * @return LoaderResolverInterface|null A LoaderResolverInterface instance + * @inheritDoc */ public function getResolver(): ?LoaderResolverInterface { @@ -47,10 +34,7 @@ public function getResolver(): ?LoaderResolverInterface } /** - * Sets the loader resolver. - * - * @param LoaderResolverInterface $resolver - * @return NullLoader + * @inheritDoc */ public function setResolver(LoaderResolverInterface $resolver): self { diff --git a/src/Routing/OptimizedNodesSourcesGraphPathAggregator.php b/src/Routing/OptimizedNodesSourcesGraphPathAggregator.php index e37cdf9d..0d171edf 100644 --- a/src/Routing/OptimizedNodesSourcesGraphPathAggregator.php +++ b/src/Routing/OptimizedNodesSourcesGraphPathAggregator.php @@ -7,22 +7,16 @@ use Doctrine\ORM\Query; use Doctrine\Persistence\ManagerRegistry; use Psr\Cache\CacheItemPoolInterface; +use Psr\Cache\InvalidArgumentException; use RZ\Roadiz\CoreBundle\Entity\Node; use RZ\Roadiz\CoreBundle\Entity\NodesSources; final class OptimizedNodesSourcesGraphPathAggregator implements NodesSourcesPathAggregator { - private ManagerRegistry $managerRegistry; - private CacheItemPoolInterface $cacheAdapter; - - /** - * @param ManagerRegistry $managerRegistry - * @param CacheItemPoolInterface $cacheAdapter - */ - public function __construct(ManagerRegistry $managerRegistry, CacheItemPoolInterface $cacheAdapter) - { - $this->managerRegistry = $managerRegistry; - $this->cacheAdapter = $cacheAdapter; + public function __construct( + private readonly ManagerRegistry $managerRegistry, + private readonly CacheItemPoolInterface $cacheAdapter + ) { } private function getCacheKey(NodesSources $nodesSources): string @@ -34,6 +28,7 @@ private function getCacheKey(NodesSources $nodesSources): string * @param NodesSources $nodesSources * @param array $parameters * @return string + * @throws InvalidArgumentException */ public function aggregatePath(NodesSources $nodesSources, array $parameters = []): string { @@ -57,9 +52,9 @@ public function aggregatePath(NodesSources $nodesSources, array $parameters = [] /** * @param Node $parent * - * @return array + * @return array */ - protected function getParentsIds(Node $parent): array + private function getParentsIds(Node $parent): array { $parentIds = []; while ($parent !== null && !$parent->isHome()) { @@ -72,13 +67,13 @@ protected function getParentsIds(Node $parent): array /** * Get every nodeSource parents identifier from current to - * farest ancestor. + * farthest ancestor. * * @param NodesSources $source * * @return array */ - protected function getIdentifiers(NodesSources $source): array + private function getIdentifiers(NodesSources $source): array { $urlTokens = []; $parents = []; diff --git a/src/Routing/RedirectableUrlMatcher.php b/src/Routing/RedirectableUrlMatcher.php index a686e08c..b6f3c486 100644 --- a/src/Routing/RedirectableUrlMatcher.php +++ b/src/Routing/RedirectableUrlMatcher.php @@ -18,7 +18,7 @@ final class RedirectableUrlMatcher extends BaseMatcher * * @return array An array of parameters */ - public function redirect($path, $route, $scheme = null): array + public function redirect(string $path, string $route, ?string $scheme = null): array { return [ '_controller' => RedirectionController::class . '::redirectToRouteAction', diff --git a/src/Routing/RedirectionMatcher.php b/src/Routing/RedirectionMatcher.php index 3579b0e1..81001bee 100644 --- a/src/Routing/RedirectionMatcher.php +++ b/src/Routing/RedirectionMatcher.php @@ -4,16 +4,13 @@ namespace RZ\Roadiz\CoreBundle\Routing; -use Doctrine\Persistence\ManagerRegistry; use Psr\Log\LoggerInterface; -use Psr\Log\NullLogger; use RZ\Roadiz\CoreBundle\Controller\RedirectionController; use RZ\Roadiz\CoreBundle\Entity\Redirection; use Symfony\Component\Routing\Exception\ResourceNotFoundException; use Symfony\Component\Routing\Matcher\UrlMatcher; use Symfony\Component\Routing\RequestContext; use Symfony\Component\Routing\RouteCollection; -use Symfony\Component\Stopwatch\Stopwatch; /** * UrlMatcher which tries to grab Node and Translation @@ -21,34 +18,19 @@ */ final class RedirectionMatcher extends UrlMatcher { - private ManagerRegistry $managerRegistry; - private Stopwatch $stopwatch; - private LoggerInterface $logger; - - /** - * @param RequestContext $context - * @param ManagerRegistry $managerRegistry - * @param Stopwatch $stopwatch - * @param LoggerInterface $logger - */ public function __construct( RequestContext $context, - ManagerRegistry $managerRegistry, - Stopwatch $stopwatch, - LoggerInterface $logger + private readonly RedirectionPathResolver $pathResolver, + private readonly LoggerInterface $logger ) { parent::__construct(new RouteCollection(), $context); - $this->stopwatch = $stopwatch; - $this->logger = $logger; - $this->managerRegistry = $managerRegistry; } /** * {@inheritdoc} */ - public function match($pathinfo): array + public function match(string $pathinfo): array { - $this->stopwatch->start('findRedirection'); $decodedUrl = rawurldecode($pathinfo); /* @@ -56,14 +38,12 @@ public function match($pathinfo): array */ if (null !== $redirection = $this->matchRedirection($decodedUrl)) { $this->logger->debug('Matched redirection.', ['query' => $redirection->getQuery()]); - $this->stopwatch->stop('findRedirection'); return [ '_controller' => RedirectionController::class . '::redirectAction', 'redirection' => $redirection, '_route' => null, ]; } - $this->stopwatch->stop('findRedirection'); throw new ResourceNotFoundException(sprintf('%s did not match any Doctrine Redirection', $pathinfo)); } @@ -74,6 +54,7 @@ public function match($pathinfo): array */ protected function matchRedirection(string $decodedUrl): ?Redirection { - return $this->managerRegistry->getRepository(Redirection::class)->findOneByQuery($decodedUrl); + $resource = $this->pathResolver->resolvePath($decodedUrl)->getResource(); + return $resource instanceof Redirection ? $resource : null; } } diff --git a/src/Routing/RedirectionPathResolver.php b/src/Routing/RedirectionPathResolver.php index b684f7f4..d6947849 100644 --- a/src/Routing/RedirectionPathResolver.php +++ b/src/Routing/RedirectionPathResolver.php @@ -5,16 +5,20 @@ namespace RZ\Roadiz\CoreBundle\Routing; use Doctrine\Persistence\ManagerRegistry; +use Psr\Cache\CacheItemPoolInterface; use RZ\Roadiz\CoreBundle\Entity\Redirection; use Symfony\Component\Routing\Exception\ResourceNotFoundException; +use Symfony\Component\Stopwatch\Stopwatch; final class RedirectionPathResolver implements PathResolverInterface { - private ManagerRegistry $managerRegistry; + public const CACHE_KEY = 'redirection_path_resolver_cache'; - public function __construct(ManagerRegistry $managerRegistry) - { - $this->managerRegistry = $managerRegistry; + public function __construct( + private readonly ManagerRegistry $managerRegistry, + private readonly CacheItemPoolInterface $cacheAdapter, + private readonly Stopwatch $stopwatch, + ) { } public function resolvePath( @@ -23,15 +27,49 @@ public function resolvePath( bool $allowRootPaths = false, bool $allowNonReachableNodes = true ): ResourceInfo { + $this->stopwatch->start('lookForRedirection', 'routing'); + $cacheItem = $this->cacheAdapter->getItem(self::CACHE_KEY); + if (!$cacheItem->isHit()) { + // Populate cache item + /** @var array[] $redirections */ + $redirections = $this->managerRegistry + ->getRepository(Redirection::class) + ->createQueryBuilder('r') + ->select(['r.id', 'r.query']) + ->getQuery() + ->getArrayResult(); + $redirections = array_combine( + array_column($redirections, 'query'), + array_column($redirections, 'id') + ); + $cacheItem->set($redirections); + $this->cacheAdapter->save($cacheItem); + } else { + /** @var array[] $redirections */ + $redirections = $cacheItem->get(); + } + + /** @var int|null $redirectionId */ + $redirectionId = $redirections[$path] ?? null; + $this->stopwatch->stop('lookForRedirection'); + + if (null === $redirectionId) { + throw new ResourceNotFoundException(); + } + $this->stopwatch->start('findRedirection', 'routing'); $redirection = $this->managerRegistry ->getRepository(Redirection::class) - ->findOneByQuery($path); - + ->find($redirectionId); + $this->stopwatch->stop('findRedirection'); if (null === $redirection) { throw new ResourceNotFoundException(); } - return (new ResourceInfo()) - ->setResource($redirection); + $this->stopwatch->start('incrementRedirection', 'routing'); + $redirection->incrementUseCount(); + $this->managerRegistry->getManagerForClass(Redirection::class)->flush(); + $this->stopwatch->stop('incrementRedirection'); + + return (new ResourceInfo())->setResource($redirection); } } diff --git a/src/Routing/RedirectionRouter.php b/src/Routing/RedirectionRouter.php index 8df68ff7..1ec14a2e 100644 --- a/src/Routing/RedirectionRouter.php +++ b/src/Routing/RedirectionRouter.php @@ -7,6 +7,7 @@ use Doctrine\Persistence\ManagerRegistry; use Psr\Log\LoggerInterface; use Symfony\Cmf\Component\Routing\VersatileGeneratorInterface; +use Symfony\Component\Routing\Exception\RouteNotFoundException; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Routing\RequestContext; use Symfony\Component\Routing\RouteCollection; @@ -15,24 +16,13 @@ class RedirectionRouter extends Router implements VersatileGeneratorInterface { - protected ManagerRegistry $managerRegistry; - protected ?Stopwatch $stopwatch; - - /** - * @param RedirectionMatcher $matcher - * @param ManagerRegistry $managerRegistry - * @param array $options - * @param RequestContext|null $context - * @param LoggerInterface|null $logger - * @param Stopwatch|null $stopwatch - */ public function __construct( RedirectionMatcher $matcher, - ManagerRegistry $managerRegistry, + protected readonly ManagerRegistry $managerRegistry, + protected readonly Stopwatch $stopwatch, array $options = [], RequestContext $context = null, LoggerInterface $logger = null, - Stopwatch $stopwatch = null ) { parent::__construct( new NullLoader(), @@ -41,8 +31,6 @@ public function __construct( $context, $logger ); - $this->stopwatch = $stopwatch; - $this->managerRegistry = $managerRegistry; $this->matcher = $matcher; } @@ -59,7 +47,7 @@ public function getRouteCollection(): RouteCollection */ public function generate(string $name, array $parameters = [], int $referenceType = self::ABSOLUTE_PATH): string { - return ''; + throw new RouteNotFoundException(get_class($this) . ' does not support path generation.'); } /** @@ -70,12 +58,7 @@ public function getGenerator(): UrlGeneratorInterface throw new \BadMethodCallException(get_class($this) . ' does not support path generation.'); } - public function supports($name): bool - { - return false; - } - - public function getRouteDebugMessage($name, array $parameters = []): string + public function getRouteDebugMessage(mixed $name, array $parameters = []): string { return 'RedirectionRouter does not support path generation.'; } diff --git a/src/Routing/StaticRouter.php b/src/Routing/StaticRouter.php deleted file mode 100644 index 4f9b917c..00000000 --- a/src/Routing/StaticRouter.php +++ /dev/null @@ -1,52 +0,0 @@ -routeCollection = $routeCollection; - } - - /** - * @return RouteCollection - */ - public function getRouteCollection(): RouteCollection - { - if (null === $this->collection) { - $this->routeCollection->parseResources(); - $this->collection = $this->routeCollection; - } - return $this->collection; - } -} diff --git a/src/SearchEngine/AbstractSearchHandler.php b/src/SearchEngine/AbstractSearchHandler.php index cd2c6f5f..133f3691 100644 --- a/src/SearchEngine/AbstractSearchHandler.php +++ b/src/SearchEngine/AbstractSearchHandler.php @@ -11,22 +11,26 @@ use Solarium\Core\Client\Client; use Solarium\Core\Query\Helper; use Solarium\QueryType\Select\Query\Query; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; abstract class AbstractSearchHandler implements SearchHandlerInterface { - private ClientRegistry $clientRegistry; + protected ClientRegistry $clientRegistry; protected ObjectManager $em; protected LoggerInterface $logger; + protected EventDispatcherInterface $eventDispatcher; protected int $highlightingFragmentSize = 150; public function __construct( ClientRegistry $clientRegistry, ObjectManager $em, - LoggerInterface $searchEngineLogger + LoggerInterface $searchEngineLogger, + EventDispatcherInterface $eventDispatcher ) { $this->clientRegistry = $clientRegistry; $this->em = $em; $this->logger = $searchEngineLogger; + $this->eventDispatcher = $eventDispatcher; } public function getSolr(): Client @@ -50,7 +54,6 @@ public function getSolr(): Client * @param array $args * @param int $rows * @param bool $searchTags Search in tags/folders too, even if a node don’t match - * @param int $proximity Proximity matching: Lucene supports finding words are a within a specific distance away. * @param int $page * * @return SearchResultsInterface Return a SearchResultsInterface iterable object. @@ -60,14 +63,13 @@ public function searchWithHighlight( array $args = [], int $rows = 20, bool $searchTags = false, - int $proximity = 1, int $page = 1 ): SearchResultsInterface { $args = $this->argFqProcess($args); $args["fq"][] = "document_type_s:" . $this->getDocumentType(); - $args["hl.q"] = $this->escapeQuery(trim($q)); + $args["hl.q"] = $this->buildHighlightingQuery($q); $args = array_merge($this->getHighlightingOptions($args), $args); - $response = $this->nativeSearch($q, $args, $rows, $searchTags, $proximity, $page); + $response = $this->nativeSearch($q, $args, $rows, $searchTags, $page); return $this->createSearchResultsFromResponse($response); } @@ -95,7 +97,7 @@ protected function getHighlightingOptions(array &$args = []): array { $tmp = []; $tmp["hl"] = true; - $tmp["hl.fl"] = $this->getCollectionField($args); + $tmp["hl.fl"] = $this->getTitleField($args) . ' ' . $this->getCollectionField($args); $tmp["hl.fragsize"] = $this->getHighlightingFragmentSize(); $tmp["hl.simple.pre"] = ''; $tmp["hl.simple.post"] = ''; @@ -148,7 +150,6 @@ public function setHighlightingFragmentSize(int $highlightingFragmentSize): Abst * @param array $args * @param int $rows * @param bool $searchTags - * @param int $proximity Proximity matching: Lucene supports finding words are a within a specific distance away. * @param int $page * * @return array|null @@ -158,7 +159,6 @@ abstract protected function nativeSearch( array $args = [], int $rows = 20, bool $searchTags = false, - int $proximity = 1, int $page = 1 ): ?array; @@ -175,7 +175,7 @@ abstract protected function nativeSearch( * ### For node-sources: * * * status (int) - * * visible (boolean) + * * visible (bool) * * nodeType (RZ\Roadiz\CoreBundle\Entity\NodeType or string or array) * * tags (RZ\Roadiz\CoreBundle\Entity\Tag or array of Tag) * * translation (RZ\Roadiz\CoreBundle\Entity\Translation) @@ -190,8 +190,7 @@ abstract protected function nativeSearch( * @param string $q * @param array $args * @param int $rows Results per page - * @param boolean $searchTags Search in tags/folders too, even if a node don’t match - * @param int $proximity Proximity matching: Lucene supports finding words are a within a specific distance away. Default 10000000 + * @param bool $searchTags Search in tags/folders too, even if a node don’t match * @param int $page Retrieve a specific page * * @return SearchResultsInterface Return an array of doctrine Entities (Document, NodesSources) @@ -201,7 +200,6 @@ public function search( array $args = [], int $rows = 20, bool $searchTags = false, - int $proximity = 1, int $page = 1 ): SearchResultsInterface { $args = $this->argFqProcess($args); @@ -209,7 +207,7 @@ public function search( $tmp = []; $args = array_merge($tmp, $args); - $response = $this->nativeSearch($q, $args, $rows, $searchTags, $proximity, $page); + $response = $this->nativeSearch($q, $args, $rows, $searchTags, $page); return $this->createSearchResultsFromResponse($response); } @@ -231,10 +229,9 @@ public function escapeQuery(string $input): string /** * @param string $q - * @param int $proximity * @return array [$exactQuery, $fuzzyQuery, $wildcardQuery] */ - protected function getFormattedQuery(string $q, int $proximity = 1): array + protected function getFormattedQuery(string $q): array { $q = trim($q); /** @@ -242,13 +239,16 @@ protected function getFormattedQuery(string $q, int $proximity = 1): array * @see https://lucene.apache.org/solr/guide/6_6/the-standard-query-parser.html#TheStandardQueryParser-FuzzySearches */ $words = preg_split('#[\s,]+#', $q, -1, PREG_SPLIT_NO_EMPTY); - $fuzzyiedQuery = implode(' ', array_map(function (string $word) use ($proximity) { + if (false === $words) { + throw new \RuntimeException('Cannot split query string.'); + } + $fuzzyiedQuery = implode(' ', array_map(function (string $word) { /* * Do not fuzz short words: Solr crashes * Proximity is set to 1 by default for single-words */ if (\mb_strlen($word) > 3) { - return $this->escapeQuery($word) . '~' . $proximity; + return $this->escapeQuery($word) . '~2'; } return $this->escapeQuery($word); }, $words)); @@ -259,7 +259,7 @@ protected function getFormattedQuery(string $q, int $proximity = 1): array /* * Wildcard search for allowing autocomplete */ - $wildcardQuery = $this->escapeQuery($q) . '*~' . $proximity; + $wildcardQuery = $this->escapeQuery($q) . '*~2'; return [$exactQuery, $fuzzyiedQuery, $wildcardQuery]; } @@ -272,15 +272,14 @@ protected function getFormattedQuery(string $q, int $proximity = 1): array * @param string $q * @param array $args * @param bool $searchTags - * @param int $proximity * @return string */ - protected function buildQuery(string $q, array &$args, bool $searchTags = false, int $proximity = 1): string + protected function buildQuery(string $q, array &$args, bool $searchTags = false): string { $titleField = $this->getTitleField($args); $collectionField = $this->getCollectionField($args); $tagsField = $this->getTagsField($args); - [$exactQuery, $fuzzyiedQuery, $wildcardQuery] = $this->getFormattedQuery($q, $proximity); + [$exactQuery, $fuzzyiedQuery, $wildcardQuery] = $this->getFormattedQuery($q); /* * Search in node-sources tags name… @@ -309,6 +308,35 @@ protected function buildQuery(string $q, array &$args, bool $searchTags = false, } } + protected function buildHighlightingQuery(string $q): string + { + $q = trim($q); + $words = preg_split('#[\s,]+#', $q, -1, PREG_SPLIT_NO_EMPTY); + if (\is_array($words) && \count($words) > 1) { + return $this->escapeQuery($q); + } + + $q = $this->escapeQuery($q); + return sprintf('%s~2', $q); + } + + /** + * @param array $args + * @param bool $searchTags + * @return string + */ + protected function buildQueryFields(array &$args, bool $searchTags = true): string + { + $titleField = $this->getTitleField($args); + $collectionField = $this->getCollectionField($args); + $tagsField = $this->getTagsField($args); + + if ($searchTags) { + return $titleField . '^10 ' . $collectionField . '^2 ' . $tagsField . ' slug_s'; + } + return $titleField . ' ' . $collectionField . ' slug_s'; + } + /** * @param string $q * diff --git a/src/SearchEngine/ClientRegistry.php b/src/SearchEngine/ClientRegistry.php index e15699dc..0e9cae6c 100644 --- a/src/SearchEngine/ClientRegistry.php +++ b/src/SearchEngine/ClientRegistry.php @@ -21,10 +21,17 @@ public function __construct(ContainerInterface $container) public function getClient(): ?Client { - return $this->container->get( + $client = $this->container->get( 'roadiz_core.solr.client', ContainerInterface::NULL_ON_INVALID_REFERENCE ); + if (null === $client) { + return null; + } + if (!($client instanceof Client)) { + throw new \RuntimeException('Solr client must be an instance of ' . Client::class); + } + return $client; } public function isClientReady(?Client $client): bool diff --git a/src/SearchEngine/DocumentSearchHandler.php b/src/SearchEngine/DocumentSearchHandler.php index 0804fbcb..eb83052a 100644 --- a/src/SearchEngine/DocumentSearchHandler.php +++ b/src/SearchEngine/DocumentSearchHandler.php @@ -6,6 +6,7 @@ use RZ\Roadiz\CoreBundle\Entity\Folder; use RZ\Roadiz\CoreBundle\Entity\Translation; +use RZ\Roadiz\CoreBundle\SearchEngine\Event\DocumentSearchQueryEvent; /** * @package RZ\Roadiz\CoreBundle\SearchEngine @@ -16,8 +17,7 @@ class DocumentSearchHandler extends AbstractSearchHandler * @param string $q * @param array $args * @param integer $rows - * @param boolean $searchTags - * @param integer $proximity Proximity matching: Lucene supports finding words are a within a specific distance away. + * @param bool $searchTags * @param integer $page * * @return array|null @@ -27,38 +27,42 @@ protected function nativeSearch( array $args = [], int $rows = 20, bool $searchTags = false, - int $proximity = 1, int $page = 1 ): ?array { - if (!empty($q)) { - $query = $this->createSolrQuery($args, $rows, $page); - $queryTxt = $this->buildQuery($q, $args, $searchTags, $proximity); - $query->setQuery($queryTxt); - - /* - * Only need these fields as Doctrine - * will do the rest. - */ - $query->setFields([ - 'id', - 'sort', - 'document_type_s', - SolariumDocumentTranslation::IDENTIFIER_KEY, - 'filename_s', - 'locale_s', - ]); - - $this->logger->debug('[Solr] Request document search…', [ - 'query' => $queryTxt, - 'fq' => $args["fq"] ?? [], - 'params' => $query->getParams(), - ]); - - $solrRequest = $this->getSolr()->execute($query); - return $solrRequest->getData(); - } else { + if (empty($q)) { return null; } + $query = $this->createSolrQuery($args, $rows, $page); + $queryTxt = $this->buildQuery($q, $args, $searchTags); + $query->setQuery($queryTxt); + + /* + * Only need these fields as Doctrine + * will do the rest. + */ + $query->setFields([ + 'id', + 'sort', + 'document_type_s', + SolariumDocumentTranslation::IDENTIFIER_KEY, + 'filename_s', + 'locale_s', + ]); + + $this->logger->debug('[Solr] Request document search…', [ + 'query' => $queryTxt, + 'fq' => $args["fq"] ?? [], + 'params' => $query->getParams(), + ]); + + /** @var DocumentSearchQueryEvent $event */ + $event = $this->eventDispatcher->dispatch( + new DocumentSearchQueryEvent($query, $args) + ); + $query = $event->getQuery(); + + $solrRequest = $this->getSolr()->execute($query); + return $solrRequest->getData(); } /** diff --git a/src/SearchEngine/Event/AbstractSearchQueryEvent.php b/src/SearchEngine/Event/AbstractSearchQueryEvent.php new file mode 100644 index 00000000..0b65abda --- /dev/null +++ b/src/SearchEngine/Event/AbstractSearchQueryEvent.php @@ -0,0 +1,36 @@ +query = $query; + $this->args = $args; + } + + /** + * @return Query + */ + public function getQuery(): Query + { + return $this->query; + } + + /** + * @return array + */ + public function getArgs(): array + { + return $this->args; + } +} diff --git a/src/SearchEngine/Event/DocumentSearchQueryEvent.php b/src/SearchEngine/Event/DocumentSearchQueryEvent.php new file mode 100644 index 00000000..b7d98ec2 --- /dev/null +++ b/src/SearchEngine/Event/DocumentSearchQueryEvent.php @@ -0,0 +1,9 @@ +> $nodesSources */ - /** @var array $nodesSources */ $nodesSources = $this->getRepository()->findBySearchQuery( $safeSearchTerms, $resultCount ); + if (count($nodesSources) > 0) { + return array_map(function (SolrSearchResultItem $item) { + return $item->getItem(); + }, $nodesSources); + } + /* * Second try with sources fields */ - if (count($nodesSources) === 0) { - $nodesSources = $this->getRepository()->searchBy( - $safeSearchTerms, - [], - [], - $resultCount - ); + $nodesSources = $this->getRepository()->searchBy( + $safeSearchTerms, + [], + [], + $resultCount + ); - if (count($nodesSources) === 0) { - /* - * Then try with node name. - */ - $qb = $this->getRepository()->createQueryBuilder('ns'); + if (count($nodesSources) === 0) { + /* + * Then try with node name. + */ + $qb = $this->getRepository()->createQueryBuilder('ns'); - $qb->select('ns, n') - ->innerJoin('ns.node', 'n') - ->andWhere($qb->expr()->orX( - $qb->expr()->like('n.nodeName', ':nodeName'), - $qb->expr()->like('ns.title', ':nodeName') - )) - ->setMaxResults($resultCount) - ->setParameter('nodeName', '%' . $safeSearchTerms . '%'); + $qb->select('ns, n') + ->innerJoin('ns.node', 'n') + ->andWhere($qb->expr()->orX( + $qb->expr()->like('n.nodeName', ':nodeName'), + $qb->expr()->like('ns.title', ':nodeName') + )) + ->setMaxResults($resultCount) + ->setParameter('nodeName', '%' . $safeSearchTerms . '%'); - if (null !== $translation) { - $qb->andWhere($qb->expr()->eq('ns.translation', ':translation')) - ->setParameter('translation', $translation); - } - try { - return $qb->getQuery()->getResult(); - } catch (NoResultException $e) { - return []; - } + if (null !== $translation) { + $qb->andWhere($qb->expr()->eq('ns.translation', ':translation')) + ->setParameter('translation', $translation); + } + try { + return $qb->getQuery()->getResult(); + } catch (NoResultException $e) { + return []; } } diff --git a/src/SearchEngine/Indexer/AbstractIndexer.php b/src/SearchEngine/Indexer/AbstractIndexer.php index ee723f7f..94e9716c 100644 --- a/src/SearchEngine/Indexer/AbstractIndexer.php +++ b/src/SearchEngine/Indexer/AbstractIndexer.php @@ -14,22 +14,16 @@ abstract class AbstractIndexer implements CliAwareIndexer { - private ClientRegistry $clientRegistry; - protected SolariumFactoryInterface $solariumFactory; protected LoggerInterface $logger; protected ?SymfonyStyle $io = null; - protected ManagerRegistry $managerRegistry; public function __construct( - ClientRegistry $clientRegistry, - ManagerRegistry $managerRegistry, - SolariumFactoryInterface $solariumFactory, - LoggerInterface $searchEngineLogger + protected readonly ClientRegistry $clientRegistry, + protected readonly ManagerRegistry $managerRegistry, + protected readonly SolariumFactoryInterface $solariumFactory, + readonly LoggerInterface $searchEngineLogger ) { - $this->solariumFactory = $solariumFactory; - $this->clientRegistry = $clientRegistry; $this->logger = $searchEngineLogger; - $this->managerRegistry = $managerRegistry; } /** diff --git a/src/SearchEngine/Indexer/DocumentIndexer.php b/src/SearchEngine/Indexer/DocumentIndexer.php index 20992dff..7118e785 100644 --- a/src/SearchEngine/Indexer/DocumentIndexer.php +++ b/src/SearchEngine/Indexer/DocumentIndexer.php @@ -61,9 +61,8 @@ public function reindexAll(): void ->createQueryBuilder('d') ->getQuery(); - if (null !== $this->io) { - $this->io->progressStart((int) $countQuery->getSingleScalarResult()); - } + $this->io?->title(get_class($this)); + $this->io?->progressStart((int) $countQuery->getSingleScalarResult()); foreach ($q->toIterable() as $row) { $solarium = $this->solariumFactory->createWithDocument($row); @@ -72,9 +71,7 @@ public function reindexAll(): void foreach ($solarium->getDocuments() as $document) { $buffer->addDocument($document); } - if (null !== $this->io) { - $this->io->progressAdvance(); - } + $this->io?->progressAdvance(); // detach from Doctrine, so that it can be Garbage-Collected immediately $this->managerRegistry->getManager()->detach($row); } @@ -83,8 +80,6 @@ public function reindexAll(): void // optimize the index $this->optimizeSolr(); - if (null !== $this->io) { - $this->io->progressFinish(); - } + $this->io?->progressFinish(); } } diff --git a/src/SearchEngine/Indexer/NodesSourcesIndexer.php b/src/SearchEngine/Indexer/NodesSourcesIndexer.php index a7780845..65279dd0 100644 --- a/src/SearchEngine/Indexer/NodesSourcesIndexer.php +++ b/src/SearchEngine/Indexer/NodesSourcesIndexer.php @@ -115,9 +115,8 @@ public function reindexAll(int $batchCount = 1, int $batchNumber = 0): void */ $paginator = new Paginator($baseQb->getQuery(), true); - if (null !== $this->io) { - $this->io->progressStart($count); - } + $this->io?->title(get_class($this)); + $this->io?->progressStart($count); foreach ($paginator as $row) { $solarium = $this->solariumFactory->createWithNodesSources($row); @@ -125,9 +124,7 @@ public function reindexAll(int $batchCount = 1, int $batchNumber = 0): void $solarium->index(); $buffer->addDocument($solarium->getDocument()); - if (null !== $this->io) { - $this->io->progressAdvance(); - } + $this->io?->progressAdvance(); // detach from Doctrine, so that it can be Garbage-Collected immediately $this->managerRegistry->getManager()->detach($row); } @@ -137,8 +134,6 @@ public function reindexAll(int $batchCount = 1, int $batchNumber = 0): void // optimize the index $this->optimizeSolr(); - if (null !== $this->io) { - $this->io->progressFinish(); - } + $this->io?->progressFinish(); } } diff --git a/src/SearchEngine/Message/AbstractSolrMessage.php b/src/SearchEngine/Message/AbstractSolrMessage.php index 16c215a0..9ce292a4 100644 --- a/src/SearchEngine/Message/AbstractSolrMessage.php +++ b/src/SearchEngine/Message/AbstractSolrMessage.php @@ -9,7 +9,8 @@ abstract class AbstractSolrMessage implements AsyncMessage { /** - * @var class-string + * Cannot typehint with class-string: breaks Symfony Serializer 5.4 + * @var string */ protected string $classname; /** @@ -17,6 +18,10 @@ abstract class AbstractSolrMessage implements AsyncMessage */ protected mixed $identifier; + /** + * @param string $classname + * @param mixed $identifier + */ public function __construct(string $classname, mixed $identifier) { $this->classname = $classname; diff --git a/src/SearchEngine/Message/Handler/SolrDeleteMessageHandler.php b/src/SearchEngine/Message/Handler/SolrDeleteMessageHandler.php index bf208a6a..587b9680 100644 --- a/src/SearchEngine/Message/Handler/SolrDeleteMessageHandler.php +++ b/src/SearchEngine/Message/Handler/SolrDeleteMessageHandler.php @@ -25,6 +25,8 @@ public function __invoke(SolrDeleteMessage $message): void { try { if (!empty($message->getIdentifier())) { + // Cannot typehint with class-string: breaks Symfony Serializer 5.4 + // @phpstan-ignore-next-line $this->indexerFactory->getIndexerFor($message->getClassname())->delete($message->getIdentifier()); } } catch (SolrServerNotAvailableException $exception) { diff --git a/src/SearchEngine/Message/Handler/SolrReindexMessageHandler.php b/src/SearchEngine/Message/Handler/SolrReindexMessageHandler.php index 15b092f7..af3e006c 100644 --- a/src/SearchEngine/Message/Handler/SolrReindexMessageHandler.php +++ b/src/SearchEngine/Message/Handler/SolrReindexMessageHandler.php @@ -25,6 +25,8 @@ public function __invoke(SolrReindexMessage $message): void { try { if (!empty($message->getIdentifier())) { + // Cannot typehint with class-string: breaks Symfony Serializer 5.4 + // @phpstan-ignore-next-line $this->indexerFactory->getIndexerFor($message->getClassname())->index($message->getIdentifier()); } } catch (SolrServerNotAvailableException $exception) { diff --git a/src/SearchEngine/NodeSourceSearchHandler.php b/src/SearchEngine/NodeSourceSearchHandler.php index 3e41672a..56c6a166 100644 --- a/src/SearchEngine/NodeSourceSearchHandler.php +++ b/src/SearchEngine/NodeSourceSearchHandler.php @@ -9,6 +9,7 @@ use RZ\Roadiz\Core\AbstractEntities\TranslationInterface; use RZ\Roadiz\CoreBundle\Entity\Node; use RZ\Roadiz\CoreBundle\Entity\Tag; +use RZ\Roadiz\CoreBundle\SearchEngine\Event\NodeSourceSearchQueryEvent; /** * @package RZ\Roadiz\CoreBundle\SearchEngine @@ -24,7 +25,6 @@ class NodeSourceSearchHandler extends AbstractSearchHandler implements NodeSourc * @param array $args * @param integer $rows * @param bool $searchTags - * @param int $proximity Proximity matching: Lucene supports finding words are a within a specific distance away. * @param int $page * * @return array|null @@ -34,52 +34,56 @@ protected function nativeSearch( array $args = [], int $rows = 20, bool $searchTags = false, - int $proximity = 1, int $page = 1 ): ?array { - if (!empty($q)) { - $query = $this->createSolrQuery($args, $rows, $page); - $queryTxt = $this->buildQuery($q, $args, $searchTags, $proximity); + if (empty($q)) { + return null; + } + $query = $this->createSolrQuery($args, $rows, $page); + $queryTxt = $this->buildQuery($q, $args, $searchTags); - if ($this->boostByPublicationDate) { - $boost = '{!boost b=recip(ms(NOW,published_at_dt),3.16e-11,1,1)}'; - $queryTxt = $boost . $queryTxt; - } - if ($this->boostByUpdateDate) { - $boost = '{!boost b=recip(ms(NOW,updated_at_dt),3.16e-11,1,1)}'; - $queryTxt = $boost . $queryTxt; - } - if ($this->boostByCreationDate) { - $boost = '{!boost b=recip(ms(NOW,created_at_dt),3.16e-11,1,1)}'; - $queryTxt = $boost . $queryTxt; - } + if ($this->boostByPublicationDate) { + $boost = '{!boost b=recip(ms(NOW,published_at_dt),3.16e-11,1,1)}'; + $queryTxt = $boost . $queryTxt; + } + if ($this->boostByUpdateDate) { + $boost = '{!boost b=recip(ms(NOW,updated_at_dt),3.16e-11,1,1)}'; + $queryTxt = $boost . $queryTxt; + } + if ($this->boostByCreationDate) { + $boost = '{!boost b=recip(ms(NOW,created_at_dt),3.16e-11,1,1)}'; + $queryTxt = $boost . $queryTxt; + } - $query->setQuery($queryTxt); + $query->setQuery($queryTxt); - /* - * Only need these fields as Doctrine - * will do the rest. - */ - $query->setFields([ - 'score', - 'id', - 'document_type_s', - SolariumNodeSource::IDENTIFIER_KEY, - 'node_name_s', - 'locale_s', - ]); + /* + * Only need these fields as Doctrine + * will do the rest. + */ + $query->setFields([ + 'score', + 'id', + 'document_type_s', + SolariumNodeSource::IDENTIFIER_KEY, + 'node_name_s', + 'locale_s', + ]); - $this->logger->debug('[Solr] Request node-sources search…', [ - 'query' => $queryTxt, - 'fq' => $args["fq"] ?? [], - 'params' => $query->getParams(), - ]); + $this->logger->debug('[Solr] Request node-sources search…', [ + 'query' => $queryTxt, + 'fq' => $args["fq"] ?? [], + 'params' => $query->getParams(), + ]); - $solrRequest = $this->getSolr()->execute($query); - return $solrRequest->getData(); - } else { - return null; - } + /** @var NodeSourceSearchQueryEvent $event */ + $event = $this->eventDispatcher->dispatch( + new NodeSourceSearchQueryEvent($query, $args) + ); + $query = $event->getQuery(); + + $solrRequest = $this->getSolr()->execute($query); + return $solrRequest->getData(); } /** @@ -162,15 +166,15 @@ protected function argFqProcess(array &$args): array */ if (isset($args['publishedAt'])) { $tmp = "published_at_dt:"; - if (!is_array($args['publishedAt']) && $args['publishedAt'] instanceof \DateTime) { + if (!is_array($args['publishedAt']) && $args['publishedAt'] instanceof \DateTimeInterface) { $tmp .= $this->formatDateTimeToUTC($args['publishedAt']); } elseif ( isset($args['publishedAt'][0]) && $args['publishedAt'][0] === "BETWEEN" && isset($args['publishedAt'][1]) && - $args['publishedAt'][1] instanceof \DateTime && + $args['publishedAt'][1] instanceof \DateTimeInterface && isset($args['publishedAt'][2]) && - $args['publishedAt'][2] instanceof \DateTime + $args['publishedAt'][2] instanceof \DateTimeInterface ) { $tmp .= "[" . $this->formatDateTimeToUTC($args['publishedAt'][1]) . @@ -180,14 +184,14 @@ protected function argFqProcess(array &$args): array isset($args['publishedAt'][0]) && $args['publishedAt'][0] === "<=" && isset($args['publishedAt'][1]) && - $args['publishedAt'][1] instanceof \DateTime + $args['publishedAt'][1] instanceof \DateTimeInterface ) { $tmp .= "[* TO " . $this->formatDateTimeToUTC($args['publishedAt'][1]) . "]"; } elseif ( isset($args['publishedAt'][0]) && $args['publishedAt'][0] === ">=" && isset($args['publishedAt'][1]) && - $args['publishedAt'][1] instanceof \DateTime + $args['publishedAt'][1] instanceof \DateTimeInterface ) { $tmp .= "[" . $this->formatDateTimeToUTC($args['publishedAt'][1]) . " TO *]"; } diff --git a/src/SearchEngine/SearchHandlerInterface.php b/src/SearchEngine/SearchHandlerInterface.php index 373c3a66..9612829c 100644 --- a/src/SearchEngine/SearchHandlerInterface.php +++ b/src/SearchEngine/SearchHandlerInterface.php @@ -11,7 +11,6 @@ interface SearchHandlerInterface * @param array $args * @param int $rows Results per page * @param bool $searchTags Search in tags/folders too, even if a node don’t match - * @param int $proximity Proximity matching: Lucene supports finding words are a within a specific distance away. Default 10000000 * @param int $page Retrieve a specific page * * @return SearchResultsInterface Return an array of doctrine Entities (Document, NodesSources) @@ -21,7 +20,6 @@ public function search( array $args = [], int $rows = 20, bool $searchTags = false, - int $proximity = 1, int $page = 1 ): SearchResultsInterface; @@ -32,7 +30,6 @@ public function search( * @param array $args * @param int $rows * @param boolean $searchTags Search in tags/folders too, even if a node don’t match - * @param int $proximity Proximity matching: Lucene supports finding words are a within a specific distance away. * @param int $page * * @return SearchResultsInterface Return a SearchResultsInterface iterable object. @@ -42,7 +39,6 @@ public function searchWithHighlight( array $args = [], int $rows = 20, bool $searchTags = false, - int $proximity = 1, int $page = 1 ): SearchResultsInterface; diff --git a/src/SearchEngine/SearchResultsInterface.php b/src/SearchEngine/SearchResultsInterface.php index a4ffb6c2..8705097a 100644 --- a/src/SearchEngine/SearchResultsInterface.php +++ b/src/SearchEngine/SearchResultsInterface.php @@ -4,9 +4,15 @@ namespace RZ\Roadiz\CoreBundle\SearchEngine; +/** + * @extends \Iterator + */ interface SearchResultsInterface extends \Iterator { public function getResultCount(): int; + /** + * @return array + */ public function getResultItems(): array; public function map(callable $callable): array; } diff --git a/src/SearchEngine/SolariumDocumentTranslation.php b/src/SearchEngine/SolariumDocumentTranslation.php index 9f0fad0a..8ea8b2fa 100644 --- a/src/SearchEngine/SolariumDocumentTranslation.php +++ b/src/SearchEngine/SolariumDocumentTranslation.php @@ -46,15 +46,16 @@ public function getDocumentId(): int|string public function getFieldsAssoc(bool $subResource = false): array { $event = new DocumentTranslationIndexingEvent($this->documentTranslation, [], $this); - - return $this->dispatcher->dispatch($event)->getAssociations(); + /** @var DocumentTranslationIndexingEvent $event */ + $event = $this->dispatcher->dispatch($event); + return $event->getAssociations(); } /** * Remove any document linked to current node-source. * * @param Query $update - * @return boolean + * @return bool */ public function clean(Query $update): bool { diff --git a/src/SearchEngine/SolariumLogger.php b/src/SearchEngine/SolariumLogger.php new file mode 100644 index 00000000..120b0a67 --- /dev/null +++ b/src/SearchEngine/SolariumLogger.php @@ -0,0 +1,187 @@ +logger = $searchEngineLogger; + $this->stopwatch = $stopwatch; + } + + public static function getSubscribedEvents(): array + { + return [ + SolariumEvents::PRE_EXECUTE_REQUEST => ['preExecuteRequest', 1000], + SolariumEvents::POST_EXECUTE_REQUEST => ['postExecuteRequest', -1000], + ]; + } + + public function log( + SolariumRequest $request, + ?SolariumResponse $response, + SolariumEndpoint $endpoint, + float $duration + ): void { + $this->queries[] = array( + 'request' => $request, + 'response' => $response, + 'duration' => $duration, + 'base_uri' => $this->getEndpointBaseUrl($endpoint), + ); + } + + public function collect(HttpRequest $request, HttpResponse $response, \Throwable $exception = null): void + { + if (isset($this->currentRequest)) { + $this->failCurrentRequest(); + } + + $time = 0; + foreach ($this->queries as $queryStruct) { + $time += $queryStruct['duration']; + } + $this->data = array( + 'queries' => $this->queries, + 'total_time' => $time, + ); + } + + public function getName(): string + { + return 'solarium'; + } + + public function getQueries(): array + { + return array_key_exists('queries', $this->data) ? $this->data['queries'] : []; + } + + public function getQueryCount(): int + { + return count($this->getQueries()); + } + + public function getTotalTime(): int + { + return array_key_exists('total_time', $this->data) ? $this->data['total_time'] : 0; + } + + public function preExecuteRequest(SolariumPreExecuteRequestEvent $event): void + { + if (isset($this->currentRequest)) { + $this->failCurrentRequest(); + } + + $this->stopwatch->start('solr', 'solr'); + + $this->currentRequest = $event->getRequest(); + $this->currentEndpoint = $event->getEndpoint(); + + $this->logger->debug($this->getEndpointBaseUrl($this->currentEndpoint) . $this->currentRequest->getUri()); + $this->currentStartTime = microtime(true); + } + + public function postExecuteRequest(SolariumPostExecuteRequestEvent $event): void + { + $endTime = microtime(true) - $this->currentStartTime; + if (!isset($this->currentRequest)) { + throw new \RuntimeException('Request not set'); + } + if ($this->currentRequest !== $event->getRequest()) { + throw new \RuntimeException('Requests differ'); + } + + if ($this->stopwatch->isStarted('solr')) { + $this->stopwatch->stop('solr'); + } + + $this->log($event->getRequest(), $event->getResponse(), $event->getEndpoint(), $endTime); + + $this->currentRequest = null; + $this->currentStartTime = null; + $this->currentEndpoint = null; + } + + public function failCurrentRequest(): void + { + $endTime = microtime(true) - $this->currentStartTime; + + if ($this->stopwatch->isStarted('solr')) { + $this->stopwatch->stop('solr'); + } + + $this->log($this->currentRequest, null, $this->currentEndpoint, $endTime); + + $this->currentRequest = null; + $this->currentStartTime = null; + $this->currentEndpoint = null; + } + + public function serialize(): string + { + return serialize($this->data); + } + + public function unserialize($data): void + { + $this->data = unserialize($data); + } + + public function reset(): void + { + $this->data = []; + $this->queries = []; + } + + public function __serialize(): array + { + return $this->data; + } + + public function __unserialize(array $data): void + { + $this->data = $data; + } + + private function getEndpointBaseUrl(SolariumEndpoint $endpoint): string + { + // Support for Solarium v4.2: getBaseUri() has been deprecated in favor of getCoreBaseUri() + return method_exists($endpoint, 'getCoreBaseUri') ? $endpoint->getCoreBaseUri() : $endpoint->getBaseUri(); + } + + public static function getTemplate(): ?string + { + return '@RoadizCore/DataCollector/solarium.html.twig'; + } +} diff --git a/src/SearchEngine/SolariumNodeSource.php b/src/SearchEngine/SolariumNodeSource.php index cf21bdf5..0c9ecf23 100644 --- a/src/SearchEngine/SolariumNodeSource.php +++ b/src/SearchEngine/SolariumNodeSource.php @@ -51,8 +51,9 @@ public function getDocumentId(): int|string public function getFieldsAssoc(bool $subResource = false): array { $event = new NodesSourcesIndexingEvent($this->nodeSource, [], $this); - - return $this->dispatcher->dispatch($event)->getAssociations(); + /** @var NodesSourcesIndexingEvent $event */ + $event = $this->dispatcher->dispatch($event); + return $event->getAssociations(); } /** diff --git a/src/SearchEngine/SolrSearchResultItem.php b/src/SearchEngine/SolrSearchResultItem.php new file mode 100644 index 00000000..9f26b97c --- /dev/null +++ b/src/SearchEngine/SolrSearchResultItem.php @@ -0,0 +1,48 @@ +> $highlighting + */ + public function __construct( + protected readonly object $item, + protected readonly array $highlighting = [] + ) { + } + + /** + * @return T + */ + #[ApiProperty] + #[Groups(['get'])] + public function getItem(): object + { + return $this->item; + } + + /** + * @return array> + */ + #[ApiProperty] + #[Groups(['get'])] + public function getHighlighting(): array + { + return $this->highlighting; + } +} diff --git a/src/SearchEngine/SolrSearchResults.php b/src/SearchEngine/SolrSearchResults.php index fe74fc47..50d7bc52 100644 --- a/src/SearchEngine/SolrSearchResults.php +++ b/src/SearchEngine/SolrSearchResults.php @@ -8,49 +8,41 @@ use JMS\Serializer\Annotation as JMS; use RZ\Roadiz\CoreBundle\Entity\DocumentTranslation; use RZ\Roadiz\CoreBundle\Entity\NodesSources; -use RZ\Roadiz\Documents\Models\DocumentInterface; +use Symfony\Component\Serializer\Attribute\Ignore; /** * Wrapper over Solr search results and metas. - * - * @package RZ\Roadiz\CoreBundle\SearchEngine */ class SolrSearchResults implements SearchResultsInterface { - /** - * @JMS\Exclude() - */ - protected array $response; - /** - * @JMS\Exclude() - */ - protected ObjectManager $entityManager; - /** - * @JMS\Exclude() - */ + #[JMS\Exclude] + #[Ignore] protected int $position; + /** - * @JMS\Exclude() + * @var array|null */ + #[JMS\Exclude] + #[Ignore] protected ?array $resultItems; - /** - * @param array $response - * @param ObjectManager $entityManager - */ - public function __construct(array $response, ObjectManager $entityManager) - { - $this->response = $response; - $this->entityManager = $entityManager; + public function __construct( + #[JMS\Exclude] + #[Ignore] + protected readonly array $response, + #[JMS\Exclude] + #[Ignore] + protected readonly ObjectManager $entityManager + ) { $this->position = 0; $this->resultItems = null; } /** * @return int - * @JMS\Groups({"search_results"}) - * @JMS\VirtualProperty() */ + #[JMS\Groups(["search_results"])] + #[JMS\VirtualProperty()] public function getResultCount(): int { if ( @@ -62,10 +54,10 @@ public function getResultCount(): int } /** - * @return array - * @JMS\Groups({"search_results"}) - * @JMS\VirtualProperty() + * @return array */ + #[JMS\Groups(["search_results"])] + #[JMS\VirtualProperty()] public function getResultItems(): array { if (null === $this->resultItems) { @@ -74,26 +66,16 @@ public function getResultItems(): array isset($this->response['response']['docs']) ) { $this->resultItems = array_filter(array_map( - function ($item) { + function (array $item) { $object = $this->getHydratedItem($item); - if (isset($this->response["highlighting"])) { - $key = 'object'; - if ($object instanceof NodesSources) { - $key = 'nodeSource'; - } - if ($object instanceof DocumentInterface) { - $key = 'document'; - } - if ($object instanceof DocumentTranslation) { - $key = 'document'; - $object = $object->getDocument(); - } - return [ - $key => $object, - 'highlighting' => $this->getHighlighting($item['id']), - ]; + if (!\is_object($object)) { + return null; } - return $object; + $highlighting = $this->getHighlighting($item['id']); + return new SolrSearchResultItem( + $object, + $highlighting + ); }, $this->response['response']['docs'] )); @@ -104,24 +86,18 @@ function ($item) { } /** - * Merge collection_txt localized fields. + * Get highlighting for one field. + * This does not merge highlighting for all fields anymore. * * @param string $id - * @return array|array[]|mixed + * @return array */ - protected function getHighlighting(string $id): mixed + protected function getHighlighting(string $id): array { - $highlights = $this->response['highlighting'][$id]; - if (!isset($highlights['collection_txt'])) { - $collectionTxt = []; - foreach ($highlights as $field => $value) { - $collectionTxt = array_merge($collectionTxt, $value); - } - $highlights = array_merge($highlights, [ - 'collection_txt' => $collectionTxt - ]); + if (isset($this->response['highlighting'][$id]) && \is_array($this->response['highlighting'][$id])) { + return $this->response['highlighting'][$id]; } - return $highlights; + return []; } /** @@ -149,10 +125,11 @@ protected function getHydratedItem(array $item): mixed $item[SolariumNodeSource::IDENTIFIER_KEY] ); case SolariumDocumentTranslation::DOCUMENT_TYPE: - return $this->entityManager->find( + $documentTranslation = $this->entityManager->find( DocumentTranslation::class, $item[SolariumDocumentTranslation::IDENTIFIER_KEY] ); + return $documentTranslation?->getDocument(); } } @@ -163,11 +140,11 @@ protected function getHydratedItem(array $item): mixed * Return the current element * * @link https://php.net/manual/en/iterator.current.php - * @return mixed Can return any type. + * @return SolrSearchResultItem * @since 5.0 */ #[\ReturnTypeWillChange] - public function current(): mixed + public function current(): SolrSearchResultItem { return $this->getResultItems()[$this->position]; } diff --git a/src/SearchEngine/Subscriber/AbstractIndexingSubscriber.php b/src/SearchEngine/Subscriber/AbstractIndexingSubscriber.php index 12aa92b0..3bb5c452 100644 --- a/src/SearchEngine/Subscriber/AbstractIndexingSubscriber.php +++ b/src/SearchEngine/Subscriber/AbstractIndexingSubscriber.php @@ -4,14 +4,63 @@ namespace RZ\Roadiz\CoreBundle\SearchEngine\Subscriber; -use RZ\Roadiz\Core\AbstractEntities\PersistableInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; -use Symfony\Component\String\Slugger\AsciiSlugger; abstract class AbstractIndexingSubscriber implements EventSubscriberInterface { + protected function flattenTextCollection(array $collection): string + { + return trim(implode(PHP_EOL, array_filter(array_unique($collection)))); + } + protected function formatDateTimeToUTC(\DateTimeInterface $dateTime): string { return gmdate('Y-m-d\TH:i:s\Z', $dateTime->getTimestamp()); } + + protected function formatGeoJsonFeature(mixed $geoJson): ?string + { + if (null === $geoJson) { + return null; + } + if (\is_string($geoJson)) { + $geoJson = \json_decode($geoJson, true); + } + if (!\is_array($geoJson)) { + return null; + } + + if ( + isset($geoJson['type']) && + $geoJson['type'] === 'Feature' && + isset($geoJson['geometry']['coordinates']) + ) { + return $geoJson['geometry']['coordinates'][1] . ',' . $geoJson['geometry']['coordinates'][0]; + } + return null; + } + + protected function formatGeoJsonFeatureCollection(mixed $geoJson): ?array + { + if (null === $geoJson) { + return null; + } + if (\is_string($geoJson)) { + $geoJson = \json_decode($geoJson, true); + } + if (!\is_array($geoJson)) { + return null; + } + if ( + isset($geoJson['type']) && + $geoJson['type'] === 'FeatureCollection' && + isset($geoJson['features']) && + \count($geoJson['features']) > 0 + ) { + return array_filter(array_map(function ($feature) { + return $this->formatGeoJsonFeature($feature); + }, $geoJson['features'])); + } + return null; + } } diff --git a/src/SearchEngine/Subscriber/AttributeValueIndexingSubscriber.php b/src/SearchEngine/Subscriber/AttributeValueIndexingSubscriber.php new file mode 100644 index 00000000..929a85e9 --- /dev/null +++ b/src/SearchEngine/Subscriber/AttributeValueIndexingSubscriber.php @@ -0,0 +1,125 @@ + ['onIndexing', 900], + ]; + } + + public function onIndexing(NodesSourcesIndexingEvent $event): void + { + if ($event->isSubResource()) { + return; + } + + $associations = $event->getAssociations(); + $attributeValues = $event->getNodeSource() + ->getNode() + ->getAttributesValuesForTranslation($event->getNodeSource()->getTranslation()); + + if ($attributeValues->count() === 0) { + return; + } + + $lang = $event->getNodeSource()->getTranslation()->getLocale(); + if ( + !\in_array( + $lang, + AbstractSolarium::$availableLocalizedTextFields + ) + ) { + $lang = null; + } + + /** @var AttributeValueInterface $attributeValue */ + foreach ($attributeValues as $attributeValue) { + if ($attributeValue->getAttribute()->isSearchable()) { + $data = $attributeValue->getAttributeValueTranslation( + $event->getNodeSource()->getTranslation() + )->getValue(); + if (null === $data) { + $data = $attributeValue->getAttributeValueTranslations()->first() ? + $attributeValue->getAttributeValueTranslations()->first()->getValue() + : null; + } + if (null !== $data) { + $fieldName = (new AsciiSlugger())->slug($attributeValue->getAttribute()->getCode())->snake()->lower()->toString(); + switch ($attributeValue->getType()) { + case AttributeInterface::INTEGER_T: + $fieldName .= '_i'; + $associations[$fieldName] = $data; + break; + case AttributeInterface::DECIMAL_T: + case AttributeInterface::PERCENT_T: + $fieldName .= '_f'; + $associations[$fieldName] = $data; + break; + case AttributeInterface::ENUM_T: + case AttributeInterface::COUNTRY_T: + case AttributeInterface::COLOUR_T: + case AttributeInterface::EMAIL_T: + $fieldName .= '_s'; + $content = $event->getSolariumDocument()->cleanTextContent($data); + $associations[$fieldName] = $content; + $associations['collection_txt'][] = $content; + if (null !== $lang) { + // Compile all text content into a single localized text field. + $associations['collection_txt_' . $lang] = $this->flattenTextCollection($associations['collection_txt']); + } + break; + case AttributeInterface::DATETIME_T: + case AttributeInterface::DATE_T: + if ($data instanceof \DateTimeInterface) { + $fieldName .= '_dt'; + $associations[$fieldName] = $this->formatDateTimeToUTC($data); + } + break; + case AttributeInterface::STRING_T: + /* + * Use locale to create field name + * with right language + */ + if (null !== $lang) { + $fieldName .= '_txt_' . $lang; + } else { + $lang = null; + $fieldName .= '_t'; + } + /* + * Strip Markdown syntax + */ + $content = $event->getSolariumDocument()->cleanTextContent($data); + if (null !== $content) { + $content = trim($content); + $associations[$fieldName] = $content; + $associations['collection_txt'][] = $content; + if (null !== $lang) { + // Compile all text content into a single localized text field. + $associations['collection_txt_' . $lang] = $this->flattenTextCollection($associations['collection_txt']); + } + } + break; + } + } + } + } + + $event->setAssociations($associations); + } +} diff --git a/src/SearchEngine/Subscriber/DefaultDocumentTranslationIndexingSubscriber.php b/src/SearchEngine/Subscriber/DefaultDocumentTranslationIndexingSubscriber.php index 66de4921..a4da9db0 100644 --- a/src/SearchEngine/Subscriber/DefaultDocumentTranslationIndexingSubscriber.php +++ b/src/SearchEngine/Subscriber/DefaultDocumentTranslationIndexingSubscriber.php @@ -112,7 +112,7 @@ public function onIndexing(DocumentTranslationIndexingEvent $event): void */ $assoc['collection_txt'] = $collection; // Compile all text content into a single localized text field. - $assoc['collection_txt_' . $lang] = implode(PHP_EOL, $collection); + $assoc['collection_txt_' . $lang] = $this->flattenTextCollection($collection); $event->setAssociations($assoc); } } diff --git a/src/SearchEngine/Subscriber/DefaultNodesSourcesIndexingSubscriber.php b/src/SearchEngine/Subscriber/DefaultNodesSourcesIndexingSubscriber.php index 0077e7ed..f86bb8e1 100644 --- a/src/SearchEngine/Subscriber/DefaultNodesSourcesIndexingSubscriber.php +++ b/src/SearchEngine/Subscriber/DefaultNodesSourcesIndexingSubscriber.php @@ -4,12 +4,11 @@ namespace RZ\Roadiz\CoreBundle\SearchEngine\Subscriber; -use Doctrine\Common\Collections\Criteria; -use RZ\Roadiz\Core\AbstractEntities\AbstractField; use RZ\Roadiz\CoreBundle\Entity\NodesSources; use RZ\Roadiz\CoreBundle\Entity\NodeTypeField; use RZ\Roadiz\CoreBundle\Entity\Tag; use RZ\Roadiz\CoreBundle\Event\NodesSources\NodesSourcesIndexingEvent; +use RZ\Roadiz\CoreBundle\SearchEngine\AbstractSolarium; use RZ\Roadiz\CoreBundle\SearchEngine\SolariumNodeSource; final class DefaultNodesSourcesIndexingSubscriber extends AbstractIndexingSubscriber @@ -32,18 +31,10 @@ public function onIndexing(NodesSourcesIndexingEvent $event): void $collection = []; $node = $nodeSource->getNode(); - if (null === $node) { - throw new \RuntimeException("No node relation found for source: " . $nodeSource->getTitle(), 1); - } - // Need a documentType field - $assoc[SolariumNodeSource::TYPE_DISCRIMINATOR] = SolariumNodeSource::DOCUMENT_TYPE; + $assoc[AbstractSolarium::TYPE_DISCRIMINATOR] = SolariumNodeSource::DOCUMENT_TYPE; // Need a nodeSourceId field $assoc[SolariumNodeSource::IDENTIFIER_KEY] = $nodeSource->getId(); - $assoc['node_type_s'] = $node->getNodeType()->getName(); - $assoc['node_name_s'] = $node->getNodeName(); - $assoc['node_status_i'] = $node->getStatus(); - $assoc['node_visible_b'] = $node->isVisible(); // Need a locale field $locale = $nodeSource->getTranslation()->getLocale(); @@ -57,17 +48,23 @@ public function onIndexing(NodesSourcesIndexingEvent $event): void $assoc['title'] = $title; $assoc['title_txt_' . $lang] = $title; - $assoc['created_at_dt'] = $this->formatDateTimeToUTC($node->getCreatedAt()); - $assoc['updated_at_dt'] = $this->formatDateTimeToUTC($node->getUpdatedAt()); - - if (null !== $nodeSource->getPublishedAt()) { - $assoc['published_at_dt'] = $this->formatDateTimeToUTC($nodeSource->getPublishedAt()); - } - /* * Do not index locale and tags if this is a sub-resource */ if (!$subResource) { + $assoc['node_type_s'] = $nodeSource->getNodeTypeName(); + $assoc['node_name_s'] = $node->getNodeName(); + $assoc['slug_s'] = $node->getNodeName(); + $assoc['node_status_i'] = $node->getStatus(); + $assoc['node_visible_b'] = $node->isVisible(); + $assoc['node_reachable_b'] = $nodeSource->isReachable(); + $assoc['created_at_dt'] = $this->formatDateTimeToUTC($node->getCreatedAt()); + $assoc['updated_at_dt'] = $this->formatDateTimeToUTC($node->getUpdatedAt()); + + if (null !== $nodeSource->getPublishedAt()) { + $assoc['published_at_dt'] = $this->formatDateTimeToUTC($nodeSource->getPublishedAt()); + } + if ($this->canIndexTitleInCollection($nodeSource)) { $collection[] = $title; } @@ -113,18 +110,61 @@ function (Tag $tag) { $allOut = array_filter(array_unique($allOut)); // Use all_tags_slugs_ss to be compatible with other data types $assoc['all_tags_slugs_ss'] = $allOut; - } - $criteria = new Criteria(); - $criteria->andWhere(Criteria::expr()->eq("type", AbstractField::BOOLEAN_T)); - $booleanFields = $node->getNodeType()->getFields()->matching($criteria); + $booleanFields = $node->getNodeType()->getFields()->filter(function (NodeTypeField $field) { + return $field->isBoolean(); + }); + $this->indexSuffixedFields($booleanFields, '_b', $nodeSource, $assoc); - /** @var NodeTypeField $booleanField */ - foreach ($booleanFields as $booleanField) { - $name = $booleanField->getName(); - $name .= '_b'; - $getter = $booleanField->getGetterName(); - $assoc[$name] = $nodeSource->$getter(); + $numberFields = $node->getNodeType()->getFields()->filter(function (NodeTypeField $field) { + return $field->isInteger(); + }); + $this->indexSuffixedFields($numberFields, '_i', $nodeSource, $assoc); + + $decimalFields = $node->getNodeType()->getFields()->filter(function (NodeTypeField $field) { + return $field->isDecimal(); + }); + $this->indexSuffixedFields($decimalFields, '_f', $nodeSource, $assoc); + + $stringFields = $node->getNodeType()->getFields()->filter(function (NodeTypeField $field) { + return $field->isEnum() || $field->isCountry() || $field->isColor() || $field->isEmail(); + }); + $this->indexSuffixedFields($stringFields, '_s', $nodeSource, $assoc); + + $dateTimeFields = $node->getNodeType()->getFields()->filter(function (NodeTypeField $field) { + return $field->isDate() || $field->isDateTime(); + }); + $this->indexSuffixedFields($dateTimeFields, '_dt', $nodeSource, $assoc); + + /* + * Make sure your Solr managed-schema has a field named `*_p` with type `location` singleValued + * + */ + $pointFields = $node->getNodeType()->getFields()->filter(function (NodeTypeField $field) { + return $field->isGeoTag(); + }); + foreach ($pointFields as $field) { + $name = $field->getName(); + $name .= '_p'; + $getter = $field->getGetterName(); + $value = $nodeSource->$getter(); + $assoc[$name] = $this->formatGeoJsonFeature($value); + } + + /* + * Make sure your Solr managed-schema has a field named `*_ps` with type `location` multiValued + * + */ + $multiPointFields = $node->getNodeType()->getFields()->filter(function (NodeTypeField $field) { + return $field->isMultiGeoTag(); + }); + foreach ($multiPointFields as $field) { + $name = $field->getName(); + $name .= '_ps'; + $getter = $field->getGetterName(); + $value = $nodeSource->$getter(); + $assoc[$name] = $this->formatGeoJsonFeatureCollection($value); + } } $searchableFields = $node->getNodeType()->getSearchableFields(); @@ -156,10 +196,32 @@ function (Tag $tag) { */ $assoc['collection_txt'] = $collection; // Compile all text content into a single localized text field. - $assoc['collection_txt_' . $lang] = implode(PHP_EOL, $collection); + $assoc['collection_txt_' . $lang] = $this->flattenTextCollection($collection); $event->setAssociations($assoc); } + /** + * @param iterable $fields + * @param string $suffix + * @param NodesSources $nodeSource + * @param array $assoc + * @return void + */ + protected function indexSuffixedFields(iterable $fields, string $suffix, NodesSources $nodeSource, array &$assoc): void + { + foreach ($fields as $field) { + $name = $field->getName(); + $name .= $suffix; + $getter = $field->getGetterName(); + $value = $nodeSource->$getter(); + if ($value instanceof \DateTimeInterface) { + $assoc[$name] = $this->formatDateTimeToUTC($value); + } elseif (null !== $value) { + $assoc[$name] = $value; + } + } + } + /** * @param NodesSources $source * @return bool @@ -173,9 +235,6 @@ protected function canIndexTitleInCollection(NodesSources $source): bool return ((bool) $source->getShowTitle()); } - if (null !== $source->getNode() && $source->getNode()->getNodeType()) { - return $source->getNode()->getNodeType()->isSearchable(); - } - return true; + return $source->getNode()->getNodeType()->isSearchable(); } } diff --git a/src/SearchEngine/Subscriber/TreeWalkerIndexingEventSubscriber.php b/src/SearchEngine/Subscriber/TreeWalkerIndexingEventSubscriber.php new file mode 100644 index 00000000..76ccc9ef --- /dev/null +++ b/src/SearchEngine/Subscriber/TreeWalkerIndexingEventSubscriber.php @@ -0,0 +1,100 @@ +walkerContext = $walkerContext; + $this->solariumFactory = $solariumFactory; + $this->maxLevel = $maxLevel; + $this->defaultLocale = $defaultLocale; + } + + /** + * @inheritDoc + */ + public static function getSubscribedEvents(): array + { + return [ + NodesSourcesIndexingEvent::class => ['onIndexing', -99], + ]; + } + + public function onIndexing(NodesSourcesIndexingEvent $event): void + { + $nodeSource = $event->getNodeSource(); + if (!$nodeSource->isReachable() || $event->isSubResource()) { + return; + } + + $assoc = $event->getAssociations(); + + $blockWalker = AutoChildrenNodeSourceWalker::build( + $nodeSource, + $this->walkerContext, + $this->maxLevel + ); + + // Need a locale field + $locale = $nodeSource->getTranslation()->getLocale(); + $lang = \Locale::getPrimaryLanguage($locale) ?? $this->defaultLocale; + + try { + foreach ($blockWalker->getChildren() as $subWalker) { + $this->walkAndIndex($subWalker, $assoc, $lang); + } + } catch (\Exception $e) { + } + + $event->setAssociations($assoc); + } + + /** + * @param WalkerInterface $walker + * @param array $assoc + * @param string $locale + * @throws \Exception + */ + protected function walkAndIndex(WalkerInterface $walker, array &$assoc, string $locale): void + { + $item = $walker->getItem(); + if ($item instanceof NodesSources) { + $solarium = $this->solariumFactory->createWithNodesSources($item); + // Fetch all fields array association AS sub-resources (i.e. do not index their title, and relationships) + $childAssoc = $solarium->getFieldsAssoc(true); + $assoc['collection_txt'] = array_filter(array_merge( + $assoc['collection_txt'], + $childAssoc['collection_txt'] + )); + $assoc['collection_txt_' . $locale] = $this->flattenTextCollection($assoc['collection_txt']); + } + if ($walker->count() > 0) { + foreach ($walker->getChildren() as $subWalker) { + $this->walkAndIndex($subWalker, $assoc, $locale); + } + } + } +} diff --git a/src/Security/Authentication/Manager/LoginAttemptManager.php b/src/Security/Authentication/Manager/LoginAttemptManager.php deleted file mode 100644 index 5e1e7ad1..00000000 --- a/src/Security/Authentication/Manager/LoginAttemptManager.php +++ /dev/null @@ -1,160 +0,0 @@ -requestStack = $requestStack; - $this->logger = $logger; - $this->managerRegistry = $managerRegistry; - } - - /** - * @param string $username - * @throws \Doctrine\ORM\NoResultException - * @throws \Doctrine\ORM\NonUniqueResultException - */ - public function checkLoginAttempts(string $username): void - { - /* - * Checks if there are more than 10 failed attempts - * from same IP address in the last 20 minutes - */ - if ( - $this->getLoginAttemptRepository()->isIpAddressBlocked( - $this->requestStack->getMainRequest()->getClientIp(), - $this->getIpAttemptGraceTime(), - $this->getIpAttemptCount() - ) - ) { - throw new TooManyLoginAttemptsException( - 'Too many login attempts for current IP address, wait before trying again.', - Response::HTTP_TOO_MANY_REQUESTS - ); - } - if ($this->getLoginAttemptRepository()->isUsernameBlocked($username)) { - throw new TooManyLoginAttemptsException( - 'Too many login attempts for this username, wait before trying again.', - Response::HTTP_TOO_MANY_REQUESTS - ); - } - } - - /** - * @param string $username - * - * @return $this - * @throws \Exception - */ - public function onFailedLoginAttempt(string $username): LoginAttemptManager - { - $manager = $this->managerRegistry->getManagerForClass(LoginAttempt::class); - if (null === $manager) { - throw new \RuntimeException('No manager found for class ' . LoginAttempt::class); - } - $loginAttempt = $this->getLoginAttemptRepository()->findOrCreateOneByIpAddressAndUsername( - $this->requestStack->getMainRequest()->getClientIp(), - $username - ); - - $loginAttempt->addAttemptCount(); - $blocksUntil = new \DateTime(); - - if ($loginAttempt->getAttemptCount() >= 9) { - $blocksUntil->add(new \DateInterval('PT30M')); - $loginAttempt->setBlocksLoginUntil($blocksUntil); - $this->logger->info(sprintf( - 'Client has been blocked from login until %s', - $blocksUntil->format('Y-m-d H:i:s') - )); - } elseif ($loginAttempt->getAttemptCount() >= 6) { - $blocksUntil->add(new \DateInterval('PT15M')); - $loginAttempt->setBlocksLoginUntil($blocksUntil); - $this->logger->info(sprintf( - 'Client has been blocked from login until %s', - $blocksUntil->format('Y-m-d H:i:s') - )); - } elseif ($loginAttempt->getAttemptCount() >= 3) { - $blocksUntil->add(new \DateInterval('PT3M')); - $loginAttempt->setBlocksLoginUntil($blocksUntil); - $this->logger->info(sprintf( - 'Client has been blocked from login until %s', - $blocksUntil->format('Y-m-d H:i:s') - )); - } - $manager->flush(); - return $this; - } - - /** - * @return LoginAttemptRepository - */ - public function getLoginAttemptRepository(): LoginAttemptRepository - { - if (null === $this->loginAttemptRepository) { - $this->loginAttemptRepository = $this->managerRegistry->getRepository(LoginAttempt::class); - } - return $this->loginAttemptRepository; - } - - /** - * @param string $username - * - * @return $this - */ - public function onSuccessLoginAttempt(string $username) - { - $this->getLoginAttemptRepository()->resetLoginAttempts( - $this->requestStack->getMainRequest()->getClientIp(), - $username - ); - return $this; - } - - /** - * @return int - */ - public function getIpAttemptGraceTime(): int - { - return $this->ipAttemptGraceTime; - } - - /** - * @return int - */ - public function getIpAttemptCount(): int - { - return $this->ipAttemptCount; - } -} diff --git a/src/Security/Authentication/RoadizAuthenticator.php b/src/Security/Authentication/RoadizAuthenticator.php index 0d4673d4..6c22dc08 100644 --- a/src/Security/Authentication/RoadizAuthenticator.php +++ b/src/Security/Authentication/RoadizAuthenticator.php @@ -16,7 +16,7 @@ use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\Exception\BadCredentialsException; -use Symfony\Component\Security\Core\Security; +use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\Security\Http\Authenticator\AbstractLoginFormAuthenticator; use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge; use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge; @@ -117,7 +117,7 @@ private function getCredentials(Request $request): array throw new BadRequestHttpException(sprintf('The key "%s" must be a string.', $this->usernamePath)); } - if (\strlen($credentials['username']) > Security::MAX_USERNAME_LENGTH) { + if (\mb_strlen($credentials['username']) > Security::MAX_USERNAME_LENGTH) { throw new BadCredentialsException('Invalid username.'); } } catch (AccessException $e) { diff --git a/src/Security/Authorization/AccessDeniedHandler.php b/src/Security/Authorization/AccessDeniedHandler.php index adf23d44..ff05c9e3 100644 --- a/src/Security/Authorization/AccessDeniedHandler.php +++ b/src/Security/Authorization/AccessDeniedHandler.php @@ -44,7 +44,7 @@ public function __construct( } /** - * Handles an access denied failure redirecting to home page + * Handles access denied failure redirecting to home page * * @param Request $request * @param AccessDeniedException $accessDeniedException diff --git a/src/Security/Authorization/Voter/GroupVoter.php b/src/Security/Authorization/Voter/GroupVoter.php index 10bcfb52..a6d8a341 100644 --- a/src/Security/Authorization/Voter/GroupVoter.php +++ b/src/Security/Authorization/Voter/GroupVoter.php @@ -13,11 +13,8 @@ class GroupVoter extends RoleVoter { - private RoleHierarchyInterface $roleHierarchy; - - public function __construct(RoleHierarchyInterface $roleHierarchy, string $prefix = 'ROLE_') + public function __construct(private readonly RoleHierarchyInterface $roleHierarchy, string $prefix = 'ROLE_') { - $this->roleHierarchy = $roleHierarchy; parent::__construct($prefix); } @@ -96,11 +93,6 @@ protected function extractGroupRoles(Group $group): array */ protected function isRoleContained(string $role, array $roles): bool { - foreach ($roles as $singleRole) { - if ($role === $singleRole) { - return true; - } - } - return false; + return \in_array($role, $roles, true); } } diff --git a/src/Security/Authorization/Voter/NodeTypeFieldVoter.php b/src/Security/Authorization/Voter/NodeTypeFieldVoter.php index 4deaa5cf..eb55732c 100644 --- a/src/Security/Authorization/Voter/NodeTypeFieldVoter.php +++ b/src/Security/Authorization/Voter/NodeTypeFieldVoter.php @@ -5,9 +5,9 @@ namespace RZ\Roadiz\CoreBundle\Security\Authorization\Voter; use RZ\Roadiz\CoreBundle\Entity\NodeTypeField; +use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\Voter\Voter; -use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Core\User\UserInterface; final class NodeTypeFieldVoter extends Voter @@ -15,7 +15,7 @@ final class NodeTypeFieldVoter extends Voter public const VIEW = 'VIEW'; public function __construct( - private Security $security + private readonly Security $security ) { } diff --git a/src/Security/Authorization/Voter/NodeVoter.php b/src/Security/Authorization/Voter/NodeVoter.php index 3655a23c..ef535c36 100644 --- a/src/Security/Authorization/Voter/NodeVoter.php +++ b/src/Security/Authorization/Voter/NodeVoter.php @@ -4,17 +4,19 @@ namespace RZ\Roadiz\CoreBundle\Security\Authorization\Voter; +use Doctrine\Persistence\ManagerRegistry; use Psr\Cache\CacheItemPoolInterface; -use RZ\Roadiz\Core\Handlers\HandlerFactoryInterface; use RZ\Roadiz\CoreBundle\Entity\Node; use RZ\Roadiz\CoreBundle\Entity\NodesSources; -use RZ\Roadiz\CoreBundle\EntityHandler\NodeHandler; use RZ\Roadiz\CoreBundle\Security\Authorization\Chroot\NodeChrootResolver; +use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\Voter\Voter; -use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Core\User\UserInterface; +/** + * @extends Voter<'CREATE'|'DUPLICATE'|'CREATE_AT_ROOT'|'SEARCH'|'READ'|'READ_AT_ROOT'|'EMPTY_TRASH'|'READ_LOGS'|'EDIT_CONTENT'|'EDIT_TAGS'|'EDIT_REALMS'|'EDIT_SETTING'|'EDIT_STATUS'|'EDIT_ATTRIBUTE'|'DELETE', Node> + */ final class NodeVoter extends Voter { public const CREATE = 'CREATE'; @@ -34,14 +36,14 @@ final class NodeVoter extends Voter public const DELETE = 'DELETE'; public function __construct( - private NodeChrootResolver $chrootResolver, - private Security $security, - private HandlerFactoryInterface $handlerFactory, - private CacheItemPoolInterface $cache + private readonly NodeChrootResolver $chrootResolver, + private readonly Security $security, + private readonly ManagerRegistry $managerRegistry, + private readonly CacheItemPoolInterface $cache ) { } - protected function supports(string $attribute, $subject): bool + protected function supports(string $attribute, mixed $subject): bool { if ( \in_array($attribute, [ @@ -79,7 +81,7 @@ protected function supports(string $attribute, $subject): bool return false; } - protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool + protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool { $user = $token->getUser(); @@ -124,9 +126,8 @@ private function isNodeInsideUserChroot(Node $node, Node $chroot, bool $includeC */ $cacheItem = $this->cache->getItem('node_offspring_ids_' . $chroot->getId()); if (!$cacheItem->isHit()) { - /** @var NodeHandler $nodeHandler */ - $nodeHandler = $this->handlerFactory->getHandler($chroot); - $offspringIds = $nodeHandler->getAllOffspringId(); + $nodeRepository = $this->managerRegistry->getRepository(Node::class); + $offspringIds = $nodeRepository->findAllOffspringIdByNode($chroot); $cacheItem->set($offspringIds); $this->cache->save($cacheItem); } else { diff --git a/src/Security/Authorization/Voter/RealmVoter.php b/src/Security/Authorization/Voter/RealmVoter.php index 7e61ec29..204508da 100644 --- a/src/Security/Authorization/Voter/RealmVoter.php +++ b/src/Security/Authorization/Voter/RealmVoter.php @@ -4,26 +4,25 @@ namespace RZ\Roadiz\CoreBundle\Security\Authorization\Voter; -use RZ\Roadiz\CoreBundle\Entity\Realm; use RZ\Roadiz\CoreBundle\Model\RealmInterface; +use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\Voter\Voter; -use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Core\User\UserInterface; +/** + * @extends Voter<'read'|'password', RealmInterface> + */ final class RealmVoter extends Voter { public const READ = 'read'; public const PASSWORD_QUERY_PARAMETER = 'password'; - private Security $security; - private RequestStack $requestStack; - - public function __construct(Security $security, RequestStack $requestStack) - { - $this->security = $security; - $this->requestStack = $requestStack; + public function __construct( + private readonly Security $security, + private readonly RequestStack $requestStack + ) { } public function supportsAttribute(string $attribute): bool @@ -31,28 +30,25 @@ public function supportsAttribute(string $attribute): bool return $attribute === self::READ; } - protected function supports(string $attribute, $subject): bool + protected function supports(string $attribute, mixed $subject): bool { return $this->supportsAttribute($attribute) && $subject instanceof RealmInterface; } /** * @param string $attribute - * @param Realm $subject + * @param RealmInterface $subject * @param TokenInterface $token * @return bool */ - public function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool + public function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool { - switch ($subject->getType()) { - case RealmInterface::TYPE_PLAIN_PASSWORD: - return $this->voteForPassword($attribute, $subject, $token); - case RealmInterface::TYPE_USER: - return $this->voteForUser($attribute, $subject, $token); - case RealmInterface::TYPE_ROLE: - return $this->voteForRole($attribute, $subject, $token); - } - return false; + return match ($subject->getType()) { + RealmInterface::TYPE_PLAIN_PASSWORD => $this->voteForPassword($attribute, $subject, $token), + RealmInterface::TYPE_USER => $this->voteForUser($attribute, $subject, $token), + RealmInterface::TYPE_ROLE => $this->voteForRole($attribute, $subject, $token), + default => false, + }; } /** @@ -61,7 +57,7 @@ public function voteOnAttribute(string $attribute, $subject, TokenInterface $tok * @param TokenInterface $token * @return bool */ - private function voteForRole(string $attribute, $subject, TokenInterface $token): bool + private function voteForRole(string $attribute, RealmInterface $subject, TokenInterface $token): bool { if (null === $role = $subject->getRole()) { return false; @@ -75,7 +71,7 @@ private function voteForRole(string $attribute, $subject, TokenInterface $token) * @param TokenInterface $token * @return bool */ - private function voteForUser(string $attribute, $subject, TokenInterface $token): bool + private function voteForUser(string $attribute, RealmInterface $subject, TokenInterface $token): bool { if ($subject->getUsers()->count() === 0 || null === $token->getUser()) { return false; @@ -91,7 +87,7 @@ private function voteForUser(string $attribute, $subject, TokenInterface $token) * @param TokenInterface $token * @return bool */ - private function voteForPassword(string $attribute, $subject, TokenInterface $token): bool + private function voteForPassword(string $attribute, RealmInterface $subject, TokenInterface $token): bool { $request = $this->requestStack->getCurrentRequest(); if (null === $request || empty($subject->getPlainPassword())) { diff --git a/src/Security/Authorization/Voter/RoleArrayVoter.php b/src/Security/Authorization/Voter/RoleArrayVoter.php index 21ae892f..b373a3c2 100644 --- a/src/Security/Authorization/Voter/RoleArrayVoter.php +++ b/src/Security/Authorization/Voter/RoleArrayVoter.php @@ -40,10 +40,8 @@ public function vote(TokenInterface $token, $subject, array $attributes): int } $result = VoterInterface::ACCESS_DENIED; - foreach ($roles as $role) { - if ($singleAttribute === $role) { - return VoterInterface::ACCESS_GRANTED; - } + if (\in_array($singleAttribute, $roles, true)) { + return VoterInterface::ACCESS_GRANTED; } } } diff --git a/src/Security/Authorization/Voter/SuperAdminRoleHierarchyVoter.php b/src/Security/Authorization/Voter/SuperAdminRoleHierarchyVoter.php index e893fcbb..23ccd845 100644 --- a/src/Security/Authorization/Voter/SuperAdminRoleHierarchyVoter.php +++ b/src/Security/Authorization/Voter/SuperAdminRoleHierarchyVoter.php @@ -10,16 +10,9 @@ final class SuperAdminRoleHierarchyVoter extends RoleArrayVoter { - private ManagerRegistry $managerRegistry; - - /** - * @param ManagerRegistry $managerRegistry - * @param string $prefix - */ - public function __construct(ManagerRegistry $managerRegistry, string $prefix = 'ROLE_') + public function __construct(private readonly ManagerRegistry $managerRegistry, string $prefix = 'ROLE_') { parent::__construct($prefix); - $this->managerRegistry = $managerRegistry; } protected function extractRoles(TokenInterface $token): array @@ -37,7 +30,10 @@ protected function extractRoles(TokenInterface $token): array private function isSuperAdmin(TokenInterface $token): bool { $roleNames = parent::extractRoles($token); - if (\in_array('ROLE_SUPER_ADMIN', $roleNames) || \in_array('ROLE_SUPERADMIN', $roleNames)) { + if ( + \in_array('ROLE_SUPER_ADMIN', $roleNames) || + \in_array('ROLE_SUPERADMIN', $roleNames) + ) { return true; } return false; diff --git a/src/Security/User/UserViewer.php b/src/Security/User/UserViewer.php index e8681701..c6c07db9 100644 --- a/src/Security/User/UserViewer.php +++ b/src/Security/User/UserViewer.php @@ -6,11 +6,10 @@ use Psr\Log\LoggerInterface; use RZ\Roadiz\CoreBundle\Bag\Settings; -use RZ\Roadiz\CoreBundle\Entity\NodesSources; use RZ\Roadiz\CoreBundle\Entity\User; use RZ\Roadiz\CoreBundle\Mailer\EmailManager; use Symfony\Cmf\Component\Routing\RouteObjectInterface; -use Symfony\Component\Mailer\Exception\TransportException; +use Symfony\Component\Mailer\Exception\TransportExceptionInterface; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Contracts\Translation\TranslatorInterface; @@ -21,7 +20,6 @@ class UserViewer protected TranslatorInterface $translator; protected EmailManager $emailManager; protected LoggerInterface $logger; - protected ?User $user = null; public function __construct( Settings $settingsBag, @@ -40,21 +38,20 @@ public function __construct( /** * Send email to reset user password. * - * @param string|NodesSources $route + * @param User $user + * @param object|string $route * @param string $htmlTemplate * @param string $txtTemplate * * @return bool - * @throws \Exception + * @throws TransportExceptionInterface */ public function sendPasswordResetLink( - $route = 'loginResetPage', + User $user, + object|string $route = 'loginResetPage', string $htmlTemplate = '@RoadizCore/email/users/reset_password_email.html.twig', string $txtTemplate = '@RoadizCore/email/users/reset_password_email.txt.twig' ): bool { - if (null === $this->user) { - throw new \InvalidArgumentException('User should be defined before sending email.'); - } $emailContact = $this->getContactEmail(); $siteName = $this->getSiteName(); @@ -62,7 +59,7 @@ public function sendPasswordResetLink( $resetLink = $this->urlGenerator->generate( $route, [ - 'token' => $this->user->getConfirmationToken(), + 'token' => $user->getConfirmationToken(), ], UrlGeneratorInterface::ABSOLUTE_URL ); @@ -71,14 +68,14 @@ public function sendPasswordResetLink( RouteObjectInterface::OBJECT_BASED_ROUTE_NAME, [ RouteObjectInterface::ROUTE_OBJECT => $route, - 'token' => $this->user->getConfirmationToken(), + 'token' => $user->getConfirmationToken(), ], UrlGeneratorInterface::ABSOLUTE_URL ); } $this->emailManager->setAssignation([ 'resetLink' => $resetLink, - 'user' => $this->user, + 'user' => $user, 'site' => $siteName, 'mailContact' => $emailContact, ]); @@ -87,18 +84,20 @@ public function sendPasswordResetLink( $this->emailManager->setSubject($this->translator->trans( 'reset.password.request' )); - $this->emailManager->setReceiver($this->user->getEmail()); - $this->emailManager->setSender([$emailContact => $siteName]); try { + $this->emailManager->setReceiver($user->getEmail()); + $this->emailManager->setSender([$emailContact => $siteName]); + // Send the message $this->emailManager->send(); return true; - } catch (TransportException $e) { + } catch (\Exception $e) { // Silent error not to prevent user creation if mailer is not configured $this->logger->error('Unable to send password reset link', [ 'exception' => get_class($e), 'message' => $e->getMessage(), + 'entity' => $user, ]); return false; } @@ -129,22 +128,4 @@ protected function getSiteName(): string return $siteName; } - - /** - * @return null|User - */ - public function getUser(): ?User - { - return $this->user; - } - - /** - * @param null|User $user - * @return UserViewer - */ - public function setUser(?User $user) - { - $this->user = $user; - return $this; - } } diff --git a/src/Serializer/CircularReferenceHandler.php b/src/Serializer/CircularReferenceHandler.php index eaec4ef9..8b5055d7 100644 --- a/src/Serializer/CircularReferenceHandler.php +++ b/src/Serializer/CircularReferenceHandler.php @@ -4,28 +4,24 @@ namespace RZ\Roadiz\CoreBundle\Serializer; -use ApiPlatform\Core\Api\IriConverterInterface; +use ApiPlatform\Api\IriConverterInterface; +use ApiPlatform\Api\UrlGeneratorInterface; final class CircularReferenceHandler { - private IriConverterInterface $iriConverter; - - /** - * @param IriConverterInterface $iriConverter - */ - public function __construct(IriConverterInterface $iriConverter) + public function __construct(private readonly IriConverterInterface $iriConverter) { - $this->iriConverter = $iriConverter; } - /** - * @param mixed $object - * @return string - */ - public function __invoke($object, string $format = null, array $context = []) + public function __invoke(mixed $object, string $format, array $context): ?string { try { - return $this->iriConverter->getIriFromItem($object); + return $this->iriConverter->getIriFromResource( + $object, + UrlGeneratorInterface::ABS_PATH, + null, + $context + ); } catch (\InvalidArgumentException $exception) { if (is_object($object) && method_exists($object, 'getId')) { return (string) $object->getId(); diff --git a/src/Serializer/Normalizer/AbstractPathNormalizer.php b/src/Serializer/Normalizer/AbstractPathNormalizer.php index e66fb474..729e3ae5 100644 --- a/src/Serializer/Normalizer/AbstractPathNormalizer.php +++ b/src/Serializer/Normalizer/AbstractPathNormalizer.php @@ -5,13 +5,12 @@ namespace RZ\Roadiz\CoreBundle\Serializer\Normalizer; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; -use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Component\Serializer\SerializerAwareInterface; use Symfony\Component\Serializer\SerializerInterface; -abstract class AbstractPathNormalizer implements ContextAwareNormalizerInterface, DenormalizerInterface, SerializerAwareInterface +abstract class AbstractPathNormalizer implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface { protected UrlGeneratorInterface $urlGenerator; /** @@ -29,27 +28,27 @@ public function __construct(NormalizerInterface $decorated, UrlGeneratorInterfac $this->urlGenerator = $urlGenerator; } - public function supportsNormalization($data, $format = null, array $context = []): bool + public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool { - return $this->decorated->supportsNormalization($data, $format); + return $this->decorated->supportsNormalization($data, $format/*, $context*/); } - public function supportsDenormalization($data, $type, $format = null): bool + public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool { - return $this->decorated->supportsDenormalization($data, $type, $format); + return $this->decorated->supportsDenormalization($data, $type, $format/*, $context*/); } /** * @param mixed $data - * @param string $class + * @param string $type * @param string|null $format * @param array $context * @return mixed * @throws \Symfony\Component\Serializer\Exception\ExceptionInterface */ - public function denormalize($data, $class, $format = null, array $context = []) + public function denormalize(mixed $data, string $type, string $format = null, array $context = []): mixed { - return $this->decorated->denormalize($data, $class, $format, $context); + return $this->decorated->denormalize($data, $type, $format, $context); } public function setSerializer(SerializerInterface $serializer): void @@ -58,4 +57,11 @@ public function setSerializer(SerializerInterface $serializer): void $this->decorated->setSerializer($serializer); } } + + public function getSupportedTypes(?string $format): array + { + return [ + '*' => false, + ]; + } } diff --git a/src/Serializer/Normalizer/AttributeValueNormalizer.php b/src/Serializer/Normalizer/AttributeValueNormalizer.php index 543d865d..b125a1a3 100644 --- a/src/Serializer/Normalizer/AttributeValueNormalizer.php +++ b/src/Serializer/Normalizer/AttributeValueNormalizer.php @@ -21,7 +21,7 @@ final class AttributeValueNormalizer extends AbstractPathNormalizer * @return array|\ArrayObject|bool|float|int|mixed|string|null * @throws \Symfony\Component\Serializer\Exception\ExceptionInterface */ - public function normalize($object, $format = null, array $context = []) + public function normalize(mixed $object, ?string $format = null, array $context = []): mixed { $data = $this->decorated->normalize($object, $format, $context); if ($object instanceof AttributeValue && is_array($data)) { @@ -31,15 +31,25 @@ public function normalize($object, $format = null, array $context = []) $data['type'] = $object->getType(); $data['code'] = $object->getAttribute()->getCode(); $data['color'] = $object->getAttribute()->getColor(); + $data['weight'] = $object->getAttribute()->getWeight(); if (isset($context['translation']) && $context['translation'] instanceof TranslationInterface) { $translatedData = $object->getAttributeValueTranslation($context['translation']); $data['label'] = $object->getAttribute()->getLabelOrCode($context['translation']); - if ($translatedData instanceof AttributeValueTranslationInterface) { + if ( + $translatedData instanceof AttributeValueTranslationInterface && + $translatedData->getValue() !== null + ) { $data['value'] = $translatedData->getValue(); + } else { + $data['value'] = $object->getAttributeValueDefaultTranslation()?->getValue(); } } + if ($data['value'] instanceof \DateTimeInterface) { + $data['value'] = $data['value']->format(\DateTimeInterface::ATOM); + } + if (\in_array('attribute_documents', $serializationGroups, true)) { $documentsContext = $context; $documentsContext['groups'] = ['document_display']; diff --git a/src/Serializer/Normalizer/CustomFormNormalizer.php b/src/Serializer/Normalizer/CustomFormNormalizer.php index 114414ea..6faf3844 100644 --- a/src/Serializer/Normalizer/CustomFormNormalizer.php +++ b/src/Serializer/Normalizer/CustomFormNormalizer.php @@ -5,7 +5,6 @@ namespace RZ\Roadiz\CoreBundle\Serializer\Normalizer; use RZ\Roadiz\CoreBundle\Entity\CustomForm; -use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface; use Symfony\Component\String\Slugger\AsciiSlugger; /** @@ -20,7 +19,7 @@ final class CustomFormNormalizer extends AbstractPathNormalizer * @return array|\ArrayObject|bool|float|int|mixed|string|null * @throws \Symfony\Component\Serializer\Exception\ExceptionInterface */ - public function normalize($object, $format = null, array $context = []) + public function normalize(mixed $object, ?string $format = null, array $context = []): mixed { $data = $this->decorated->normalize($object, $format, $context); if ($object instanceof CustomForm && is_array($data)) { diff --git a/src/Serializer/Normalizer/DocumentNormalizer.php b/src/Serializer/Normalizer/DocumentNormalizer.php index 918e6aee..b11d4d95 100644 --- a/src/Serializer/Normalizer/DocumentNormalizer.php +++ b/src/Serializer/Normalizer/DocumentNormalizer.php @@ -8,7 +8,6 @@ use RZ\Roadiz\Core\AbstractEntities\TranslationInterface; use RZ\Roadiz\CoreBundle\Entity\Document; use RZ\Roadiz\CoreBundle\Entity\DocumentTranslation; -use RZ\Roadiz\CoreBundle\Entity\Folder; use RZ\Roadiz\Documents\MediaFinders\EmbedFinderFactory; use RZ\Roadiz\Documents\Models\FolderInterface; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; @@ -19,18 +18,13 @@ */ final class DocumentNormalizer extends AbstractPathNormalizer { - private FilesystemOperator $documentsStorage; - private EmbedFinderFactory $embedFinderFactory; - public function __construct( - FilesystemOperator $documentsStorage, NormalizerInterface $decorated, UrlGeneratorInterface $urlGenerator, - EmbedFinderFactory $embedFinderFactory + private readonly FilesystemOperator $documentsStorage, + private readonly EmbedFinderFactory $embedFinderFactory ) { parent::__construct($decorated, $urlGenerator); - $this->documentsStorage = $documentsStorage; - $this->embedFinderFactory = $embedFinderFactory; } /** @@ -40,7 +34,7 @@ public function __construct( * @return array|\ArrayObject|bool|float|int|string|null * @throws \Symfony\Component\Serializer\Exception\ExceptionInterface */ - public function normalize($object, $format = null, array $context = []) + public function normalize(mixed $object, ?string $format = null, array $context = []): mixed { $data = $this->decorated->normalize($object, $format, $context); if ( @@ -61,6 +55,14 @@ public function normalize($object, $format = null, array $context = []) } } + if ( + !$object->isPrivate() && + $object->isProcessable() && + null !== $alignment = $object->getImageCropAlignment() + ) { + $data['imageCropAlignment'] = $alignment; + } + if ( \in_array('document_folders_all', $serializationGroups, true) ) { diff --git a/src/Serializer/Normalizer/DocumentSourcesNormalizer.php b/src/Serializer/Normalizer/DocumentSourcesNormalizer.php index e61ef468..e52f4004 100644 --- a/src/Serializer/Normalizer/DocumentSourcesNormalizer.php +++ b/src/Serializer/Normalizer/DocumentSourcesNormalizer.php @@ -11,15 +11,12 @@ final class DocumentSourcesNormalizer extends AbstractPathNormalizer { - protected DocumentFinderInterface $documentFinder; - public function __construct( NormalizerInterface $decorated, UrlGeneratorInterface $urlGenerator, - DocumentFinderInterface $documentFinder + private readonly DocumentFinderInterface $documentFinder ) { parent::__construct($decorated, $urlGenerator); - $this->documentFinder = $documentFinder; } /** @@ -29,7 +26,7 @@ public function __construct( * @return array|\ArrayObject|bool|float|int|mixed|string|null * @throws \Symfony\Component\Serializer\Exception\ExceptionInterface */ - public function normalize($object, $format = null, array $context = []) + public function normalize(mixed $object, ?string $format = null, array $context = []): mixed { $data = $this->decorated->normalize($object, $format, $context); if ($object instanceof Document && is_array($data)) { diff --git a/src/Serializer/Normalizer/FolderNormalizer.php b/src/Serializer/Normalizer/FolderNormalizer.php index b50a7642..a3f86112 100644 --- a/src/Serializer/Normalizer/FolderNormalizer.php +++ b/src/Serializer/Normalizer/FolderNormalizer.php @@ -20,7 +20,7 @@ final class FolderNormalizer extends AbstractPathNormalizer * @return array|\ArrayObject|bool|float|int|mixed|string|null * @throws \Symfony\Component\Serializer\Exception\ExceptionInterface */ - public function normalize($object, $format = null, array $context = []) + public function normalize(mixed $object, ?string $format = null, array $context = []): mixed { $data = $this->decorated->normalize($object, $format, $context); if ($object instanceof Folder && is_array($data)) { diff --git a/src/Serializer/Normalizer/NodesSourcesPathNormalizer.php b/src/Serializer/Normalizer/NodesSourcesPathNormalizer.php index e6c63040..650ff2e0 100644 --- a/src/Serializer/Normalizer/NodesSourcesPathNormalizer.php +++ b/src/Serializer/Normalizer/NodesSourcesPathNormalizer.php @@ -17,7 +17,7 @@ final class NodesSourcesPathNormalizer extends AbstractPathNormalizer * @return array|\ArrayObject|bool|float|int|mixed|string|null * @throws \Symfony\Component\Serializer\Exception\ExceptionInterface */ - public function normalize($object, $format = null, array $context = []) + public function normalize(mixed $object, ?string $format = null, array $context = []): mixed { $data = $this->decorated->normalize($object, $format, $context); if ( diff --git a/src/Serializer/Normalizer/RealmSerializationGroupNormalizer.php b/src/Serializer/Normalizer/RealmSerializationGroupNormalizer.php index f51583b7..a470c7eb 100644 --- a/src/Serializer/Normalizer/RealmSerializationGroupNormalizer.php +++ b/src/Serializer/Normalizer/RealmSerializationGroupNormalizer.php @@ -4,52 +4,59 @@ namespace RZ\Roadiz\CoreBundle\Serializer\Normalizer; -use Doctrine\Persistence\ManagerRegistry; use RZ\Roadiz\CoreBundle\Entity\NodesSources; -use RZ\Roadiz\CoreBundle\Entity\Realm; +use RZ\Roadiz\CoreBundle\Model\RealmInterface; +use RZ\Roadiz\CoreBundle\Realm\RealmResolver; use RZ\Roadiz\CoreBundle\Security\Authorization\Voter\RealmVoter; -use Symfony\Component\Security\Core\Security; -use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface; +use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface; use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; +use Symfony\Component\Stopwatch\Stopwatch; -final class RealmSerializationGroupNormalizer implements ContextAwareNormalizerInterface, NormalizerAwareInterface +final class RealmSerializationGroupNormalizer implements NormalizerInterface, NormalizerAwareInterface { use NormalizerAwareTrait; private const ALREADY_CALLED = 'REALM_SERIALIZER_NORMALIZER_ALREADY_CALLED'; - private Security $security; - private ManagerRegistry $managerRegistry; - /** - * @param Security $security - * @param ManagerRegistry $managerRegistry - */ - public function __construct(Security $security, ManagerRegistry $managerRegistry) - { - $this->security = $security; - $this->managerRegistry = $managerRegistry; + public function __construct( + private readonly Security $security, + private readonly RealmResolver $realmResolver, + private readonly Stopwatch $stopwatch + ) { } /** * @inheritDoc */ - public function supportsNormalization($data, string $format = null, array $context = []): bool + public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool { + if (!($data instanceof NodesSources)) { + return false; + } // Make sure we're not called twice if (isset($context[self::ALREADY_CALLED])) { return false; } - return $data instanceof NodesSources; + return $this->realmResolver->hasRealmsWithSerializationGroup(); + } + + public function getSupportedTypes(?string $format): array + { + return [ + '*' => false, + ]; } /** * @inheritDoc * @return array|string|int|float|bool|\ArrayObject|null */ - public function normalize($object, string $format = null, array $context = []) + public function normalize(mixed $object, ?string $format = null, array $context = []): mixed { + $this->stopwatch->start('realm-serialization-group-normalizer', 'serializer'); $realms = $this->getAuthorizedRealmsForObject($object); foreach ($realms as $realm) { @@ -59,18 +66,19 @@ public function normalize($object, string $format = null, array $context = []) } $context[self::ALREADY_CALLED] = true; + $this->stopwatch->stop('realm-serialization-group-normalizer'); return $this->normalizer->normalize($object, $format, $context); } /** - * @return Realm[] + * @return RealmInterface[] */ private function getAuthorizedRealmsForObject(NodesSources $object): array { - $realms = $this->managerRegistry->getRepository(Realm::class)->findByNode($object->getNode()); + $realms = $this->realmResolver->getRealmsWithSerializationGroup($object->getNode()); - return array_filter($realms, function (Realm $realm) { + return array_filter($realms, function (RealmInterface $realm) { return $this->security->isGranted(RealmVoter::READ, $realm); }); } diff --git a/src/Serializer/Normalizer/TagNormalizer.php b/src/Serializer/Normalizer/TagNormalizer.php index aa6aa71a..da9a11c7 100644 --- a/src/Serializer/Normalizer/TagNormalizer.php +++ b/src/Serializer/Normalizer/TagNormalizer.php @@ -21,27 +21,28 @@ final class TagNormalizer extends AbstractPathNormalizer * @return array|\ArrayObject|bool|float|int|mixed|string|null * @throws \Symfony\Component\Serializer\Exception\ExceptionInterface */ - public function normalize($object, $format = null, array $context = []) + public function normalize(mixed $object, ?string $format = null, array $context = []): mixed { $data = $this->decorated->normalize($object, $format, $context); - if ($object instanceof Tag && is_array($data)) { - $data['slug'] = $object->getTagName(); + if ( + $object instanceof Tag && + is_array($data) && + isset($context['translation']) && + $context['translation'] instanceof TranslationInterface + ) { /** @var array $serializationGroups */ $serializationGroups = isset($context['groups']) && is_array($context['groups']) ? $context['groups'] : []; + $translatedData = $object->getTranslatedTagsByTranslation($context['translation'])->first() ?: null; + if ($translatedData instanceof TagTranslation) { + $data['name'] = $translatedData->getName(); + $data['description'] = $translatedData->getDescription(); - if (isset($context['translation']) && $context['translation'] instanceof TranslationInterface) { - $documentsContext = $context; - $documentsContext['groups'] = ['document_display']; - $translatedData = $object->getTranslatedTagsByTranslation($context['translation'])->first() ?: null; - if ($translatedData instanceof TagTranslation) { - $data['name'] = $translatedData->getName(); - $data['description'] = $translatedData->getDescription(); - - if (\in_array('tag_documents', $serializationGroups, true)) { - $data['documents'] = array_map(function (DocumentInterface $document) use ($format, $documentsContext) { - return $this->decorated->normalize($document, $format, $documentsContext); - }, $translatedData->getDocuments()); - } + if (\in_array('tag_documents', $serializationGroups, true)) { + $documentsContext = $context; + $documentsContext['groups'] = ['document_display']; + $data['documents'] = array_map(function (DocumentInterface $document) use ($format, $documentsContext) { + return $this->decorated->normalize($document, $format, $documentsContext); + }, $translatedData->getDocuments()); } } } diff --git a/src/Serializer/Normalizer/TranslationAwareNormalizer.php b/src/Serializer/Normalizer/TranslationAwareNormalizer.php index a0441cc9..eb1c3df2 100644 --- a/src/Serializer/Normalizer/TranslationAwareNormalizer.php +++ b/src/Serializer/Normalizer/TranslationAwareNormalizer.php @@ -12,28 +12,21 @@ use RZ\Roadiz\CoreBundle\Preview\PreviewResolverInterface; use RZ\Roadiz\CoreBundle\Repository\TranslationRepository; use Symfony\Component\HttpFoundation\RequestStack; -use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface; use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait; -final class TranslationAwareNormalizer implements ContextAwareNormalizerInterface, NormalizerAwareInterface +final class TranslationAwareNormalizer implements NormalizerInterface, NormalizerAwareInterface { use NormalizerAwareTrait; - private RequestStack $requestStack; - private ManagerRegistry $managerRegistry; - private PreviewResolverInterface $previewResolver; - private const ALREADY_CALLED = 'TRANSLATION_AWARE_NORMALIZER_ALREADY_CALLED'; public function __construct( - RequestStack $requestStack, - ManagerRegistry $managerRegistry, - PreviewResolverInterface $previewResolver + private readonly RequestStack $requestStack, + private readonly ManagerRegistry $managerRegistry, + private readonly PreviewResolverInterface $previewResolver ) { - $this->requestStack = $requestStack; - $this->managerRegistry = $managerRegistry; - $this->previewResolver = $previewResolver; } /** @@ -43,7 +36,7 @@ public function __construct( * @return array|\ArrayObject|bool|float|int|string|null * @throws \Symfony\Component\Serializer\Exception\ExceptionInterface */ - public function normalize($object, $format = null, array $context = []) + public function normalize(mixed $object, ?string $format = null, array $context = []): mixed { if ($object instanceof WebResponseInterface) { $item = $object->getItem(); @@ -82,21 +75,33 @@ private function getTranslationFromLocale(string $locale): ?TranslationInterface private function getTranslationFromRequest(): ?TranslationInterface { $request = $this->requestStack->getMainRequest(); + + if (null === $request) { + return $this->managerRegistry + ->getRepository(Translation::class) + ->findDefault(); + } + + /* + * Try to get translation resolved from LocaleSubscriber before + */ + $requestTranslation = $request->attributes->get('_translation'); + if ($requestTranslation instanceof TranslationInterface) { + return $requestTranslation; + } + + $locale = $request->query->get('_locale', $request->getLocale()); if ( - null !== $request && - null !== $translation = $this->getTranslationFromLocale( - $request->query->get('_locale', $request->getLocale()) - ) + \is_string($locale) && + null !== $translation = $this->getTranslationFromLocale($locale) ) { return $translation; } - return $this->managerRegistry - ->getRepository(Translation::class) - ->findDefault(); + return null; } - public function supportsNormalization($data, $format = null, array $context = []): bool + public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool { // Make sure we're not called twice if (isset($context[self::ALREADY_CALLED])) { @@ -105,4 +110,11 @@ public function supportsNormalization($data, $format = null, array $context = [] return true; } + + public function getSupportedTypes(?string $format): array + { + return [ + '*' => false, + ]; + } } diff --git a/src/Serializer/ObjectConstructor/AbstractTypedObjectConstructor.php b/src/Serializer/ObjectConstructor/AbstractTypedObjectConstructor.php index 9fb1f1ea..476a75dd 100644 --- a/src/Serializer/ObjectConstructor/AbstractTypedObjectConstructor.php +++ b/src/Serializer/ObjectConstructor/AbstractTypedObjectConstructor.php @@ -13,17 +13,10 @@ abstract class AbstractTypedObjectConstructor implements TypedObjectConstructorInterface { - protected ObjectManager $entityManager; - protected ObjectConstructorInterface $fallbackConstructor; - - /** - * @param ObjectManager $entityManager - * @param ObjectConstructorInterface $fallbackConstructor - */ - public function __construct(ObjectManager $entityManager, ObjectConstructorInterface $fallbackConstructor) - { - $this->entityManager = $entityManager; - $this->fallbackConstructor = $fallbackConstructor; + public function __construct( + protected readonly ObjectManager $entityManager, + protected readonly ObjectConstructorInterface $fallbackConstructor + ) { } /** @@ -32,7 +25,7 @@ public function __construct(ObjectManager $entityManager, ObjectConstructorInter * * @return object|null */ - abstract protected function findObject($data, DeserializationContext $context): ?object; + abstract protected function findObject(mixed $data, DeserializationContext $context): ?object; /** * @param object $object diff --git a/src/Serializer/ObjectConstructor/AttributeObjectConstructor.php b/src/Serializer/ObjectConstructor/AttributeObjectConstructor.php new file mode 100644 index 00000000..ba437452 --- /dev/null +++ b/src/Serializer/ObjectConstructor/AttributeObjectConstructor.php @@ -0,0 +1,55 @@ +entityManager + ->getRepository(AttributeInterface::class) + ->findOneByCode($data['code']); + + if ( + null !== $tag && + $context->hasAttribute(self::EXCEPTION_ON_EXISTING) && + true === $context->hasAttribute(self::EXCEPTION_ON_EXISTING) + ) { + throw new EntityAlreadyExistsException('Attribute already exists in database.'); + } + + return $tag; + } + + protected function fillIdentifier(object $object, array $data): void + { + if ($object instanceof AttributeInterface) { + $object->setCode($data['code']); + } + } +} diff --git a/src/Serializer/ObjectConstructor/ChainDoctrineObjectConstructor.php b/src/Serializer/ObjectConstructor/ChainDoctrineObjectConstructor.php index 5e3fc28a..eeff57cc 100644 --- a/src/Serializer/ObjectConstructor/ChainDoctrineObjectConstructor.php +++ b/src/Serializer/ObjectConstructor/ChainDoctrineObjectConstructor.php @@ -11,28 +11,13 @@ use JMS\Serializer\Visitor\DeserializationVisitorInterface; use RZ\Roadiz\Core\AbstractEntities\PersistableInterface; -class ChainDoctrineObjectConstructor implements ObjectConstructorInterface +final class ChainDoctrineObjectConstructor implements ObjectConstructorInterface { - protected ?ObjectManager $entityManager; - /** - * @var array - */ - protected array $typedObjectConstructors; - protected ObjectConstructorInterface $fallbackConstructor; - - /** - * @param ObjectManager|null $entityManager - * @param ObjectConstructorInterface $fallbackConstructor - * @param array $typedObjectConstructors - */ public function __construct( - ?ObjectManager $entityManager, - ObjectConstructorInterface $fallbackConstructor, - array $typedObjectConstructors + private readonly ?ObjectManager $entityManager, + private readonly ObjectConstructorInterface $fallbackConstructor, + private readonly array $typedObjectConstructors ) { - $this->entityManager = $entityManager; - $this->typedObjectConstructors = $typedObjectConstructors; - $this->fallbackConstructor = $fallbackConstructor; } /** @@ -57,9 +42,11 @@ public function construct( // Locate possible ClassMetadata $classMetadataFactory = $this->entityManager->getMetadataFactory(); + /** @var class-string $className */ + $className = $metadata->name; try { - $doctrineMetadata = $classMetadataFactory->getMetadataFor($metadata->name); - if ($doctrineMetadata->getName() !== $metadata->name) { + $doctrineMetadata = $classMetadataFactory->getMetadataFor($className); + if ($doctrineMetadata->getName() !== $className) { /* * Doctrine resolveTargetEntity has found an alternative class */ @@ -69,7 +56,9 @@ public function construct( // Object class is not a valid doctrine entity } - if ($classMetadataFactory->isTransient($metadata->name)) { + /** @var class-string $className */ + $className = $metadata->name; + if ($classMetadataFactory->isTransient($className)) { // No ClassMetadata found, proceed with normal deserialization return $this->fallbackConstructor->construct($visitor, $metadata, $data, $type, $context); } @@ -77,12 +66,12 @@ public function construct( // Managed entity, check for proxy load if (!\is_array($data)) { // Single identifier, load proxy - return $this->entityManager->getReference($metadata->name, $data); + return $this->entityManager->getReference($className, $data); } /** @var TypedObjectConstructorInterface $typedObjectConstructor */ foreach ($this->typedObjectConstructors as $typedObjectConstructor) { - if ($typedObjectConstructor->supports($metadata->name, $data)) { + if ($typedObjectConstructor->supports($className, $data)) { return $typedObjectConstructor->construct( $visitor, $metadata, @@ -93,17 +82,12 @@ public function construct( } } - // PHPStan need to explicit classname - /** @var class-string $className */ - $className = $metadata->name; - // Fallback to default constructor if missing identifier(s) $classMetadata = $this->entityManager->getClassMetadata($className); $identifierList = []; foreach ($classMetadata->getIdentifierFieldNames() as $name) { if ( - isset($metadata->propertyMetadata[$name]) && isset($metadata->propertyMetadata[$name]->serializedName) ) { $dataName = $metadata->propertyMetadata[$name]->serializedName; diff --git a/src/Serializer/ObjectConstructor/GroupObjectConstructor.php b/src/Serializer/ObjectConstructor/GroupObjectConstructor.php index 8d0fbf1e..9cd71848 100644 --- a/src/Serializer/ObjectConstructor/GroupObjectConstructor.php +++ b/src/Serializer/ObjectConstructor/GroupObjectConstructor.php @@ -8,7 +8,7 @@ use JMS\Serializer\Exception\ObjectConstructionException; use RZ\Roadiz\CoreBundle\Entity\Group; -class GroupObjectConstructor extends AbstractTypedObjectConstructor +final class GroupObjectConstructor extends AbstractTypedObjectConstructor { /** * @inheritDoc @@ -21,7 +21,7 @@ public function supports(string $className, array $data): bool /** * @inheritDoc */ - protected function findObject($data, DeserializationContext $context): ?object + protected function findObject(mixed $data, DeserializationContext $context): ?object { if (null === $data['name'] || $data['name'] === '') { throw new ObjectConstructionException('Group name can not be empty'); diff --git a/src/Serializer/ObjectConstructor/NodeObjectConstructor.php b/src/Serializer/ObjectConstructor/NodeObjectConstructor.php index 4a9412dc..ec0cc75c 100644 --- a/src/Serializer/ObjectConstructor/NodeObjectConstructor.php +++ b/src/Serializer/ObjectConstructor/NodeObjectConstructor.php @@ -9,7 +9,7 @@ use RZ\Roadiz\CoreBundle\Entity\Node; use RZ\Roadiz\CoreBundle\Repository\NodeRepository; -class NodeObjectConstructor extends AbstractTypedObjectConstructor +final class NodeObjectConstructor extends AbstractTypedObjectConstructor { /** * @inheritDoc @@ -22,7 +22,7 @@ public function supports(string $className, array $data): bool /** * @inheritDoc */ - protected function findObject($data, DeserializationContext $context): ?object + protected function findObject(mixed $data, DeserializationContext $context): ?object { if (empty($data['nodeName']) && empty($data['node_name'])) { throw new ObjectConstructionException('Node name can not be empty'); diff --git a/src/Serializer/ObjectConstructor/NodeTypeFieldObjectConstructor.php b/src/Serializer/ObjectConstructor/NodeTypeFieldObjectConstructor.php deleted file mode 100644 index 71fddc76..00000000 --- a/src/Serializer/ObjectConstructor/NodeTypeFieldObjectConstructor.php +++ /dev/null @@ -1,64 +0,0 @@ -entityManager - ->getRepository(NodeType::class) - ->findOneByName($data['nodeTypeName'] ?? $data['node_type_name']); - - if (null === $nodeType) { - /* - * Do not look for existing fields if node-type does not exist either. - */ - return null; - } - return $this->entityManager - ->getRepository(NodeTypeField::class) - ->findOneBy([ - 'name' => $data['name'], - 'nodeType' => $nodeType, - ]); - } - - protected function fillIdentifier(object $object, array $data): void - { - trigger_error('Cannot call fillIdentifier on NodeTypeField', E_USER_WARNING); - } - - /** - * @return bool - */ - protected function canBeFlushed(): bool - { - return false; - } -} diff --git a/src/Serializer/ObjectConstructor/NodeTypeObjectConstructor.php b/src/Serializer/ObjectConstructor/NodeTypeObjectConstructor.php index 71d96815..4d51fccd 100644 --- a/src/Serializer/ObjectConstructor/NodeTypeObjectConstructor.php +++ b/src/Serializer/ObjectConstructor/NodeTypeObjectConstructor.php @@ -6,22 +6,24 @@ use JMS\Serializer\DeserializationContext; use JMS\Serializer\Exception\ObjectConstructionException; +use RZ\Roadiz\Contracts\NodeType\NodeTypeInterface; use RZ\Roadiz\CoreBundle\Entity\NodeType; -class NodeTypeObjectConstructor extends AbstractTypedObjectConstructor +final class NodeTypeObjectConstructor extends AbstractTypedObjectConstructor { /** * @inheritDoc */ public function supports(string $className, array $data): bool { - return $className === NodeType::class && array_key_exists('name', $data); + return \is_subclass_of($className, NodeTypeInterface::class) && + array_key_exists('name', $data); } /** * @inheritDoc */ - protected function findObject($data, DeserializationContext $context): ?object + protected function findObject(mixed $data, DeserializationContext $context): ?object { if (null === $data['name'] || $data['name'] === '') { throw new ObjectConstructionException('NodeType name can not be empty'); diff --git a/src/Serializer/ObjectConstructor/RoleObjectConstructor.php b/src/Serializer/ObjectConstructor/RoleObjectConstructor.php index 492348be..00698c86 100644 --- a/src/Serializer/ObjectConstructor/RoleObjectConstructor.php +++ b/src/Serializer/ObjectConstructor/RoleObjectConstructor.php @@ -8,7 +8,7 @@ use JMS\Serializer\Exception\ObjectConstructionException; use RZ\Roadiz\CoreBundle\Entity\Role; -class RoleObjectConstructor extends AbstractTypedObjectConstructor +final class RoleObjectConstructor extends AbstractTypedObjectConstructor { /** * @inheritDoc @@ -21,7 +21,7 @@ public function supports(string $className, array $data): bool /** * @inheritDoc */ - protected function findObject($data, DeserializationContext $context): ?object + protected function findObject(mixed $data, DeserializationContext $context): ?object { if (null === $data['name'] || $data['name'] === '') { throw new ObjectConstructionException('Role name can not be empty'); diff --git a/src/Serializer/ObjectConstructor/SettingGroupObjectConstructor.php b/src/Serializer/ObjectConstructor/SettingGroupObjectConstructor.php index 0b6a8adb..19834d39 100644 --- a/src/Serializer/ObjectConstructor/SettingGroupObjectConstructor.php +++ b/src/Serializer/ObjectConstructor/SettingGroupObjectConstructor.php @@ -8,7 +8,7 @@ use JMS\Serializer\Exception\ObjectConstructionException; use RZ\Roadiz\CoreBundle\Entity\SettingGroup; -class SettingGroupObjectConstructor extends AbstractTypedObjectConstructor +final class SettingGroupObjectConstructor extends AbstractTypedObjectConstructor { /** * @inheritDoc @@ -21,7 +21,7 @@ public function supports(string $className, array $data): bool /** * @inheritDoc */ - protected function findObject($data, DeserializationContext $context): ?object + protected function findObject(mixed $data, DeserializationContext $context): ?object { if (null === $data['name'] || $data['name'] === '') { throw new ObjectConstructionException('SettingGroup name can not be empty'); diff --git a/src/Serializer/ObjectConstructor/SettingObjectConstructor.php b/src/Serializer/ObjectConstructor/SettingObjectConstructor.php index 7db87eb5..c43a7384 100644 --- a/src/Serializer/ObjectConstructor/SettingObjectConstructor.php +++ b/src/Serializer/ObjectConstructor/SettingObjectConstructor.php @@ -8,7 +8,7 @@ use JMS\Serializer\Exception\ObjectConstructionException; use RZ\Roadiz\CoreBundle\Entity\Setting; -class SettingObjectConstructor extends AbstractTypedObjectConstructor +final class SettingObjectConstructor extends AbstractTypedObjectConstructor { /** * @inheritDoc @@ -21,7 +21,7 @@ public function supports(string $className, array $data): bool /** * @inheritDoc */ - protected function findObject($data, DeserializationContext $context): ?object + protected function findObject(mixed $data, DeserializationContext $context): ?object { if (null === $data['name'] || $data['name'] === '') { throw new ObjectConstructionException('Setting name can not be empty'); diff --git a/src/Serializer/ObjectConstructor/TagObjectConstructor.php b/src/Serializer/ObjectConstructor/TagObjectConstructor.php index 126ed722..cc984c9a 100644 --- a/src/Serializer/ObjectConstructor/TagObjectConstructor.php +++ b/src/Serializer/ObjectConstructor/TagObjectConstructor.php @@ -9,7 +9,7 @@ use RZ\Roadiz\CoreBundle\Entity\Tag; use RZ\Roadiz\CoreBundle\Exception\EntityAlreadyExistsException; -class TagObjectConstructor extends AbstractTypedObjectConstructor +final class TagObjectConstructor extends AbstractTypedObjectConstructor { public const EXCEPTION_ON_EXISTING_TAG = 'exception_on_existing_tag'; @@ -27,7 +27,7 @@ public function supports(string $className, array $data): bool /** * @inheritDoc */ - protected function findObject($data, DeserializationContext $context): ?object + protected function findObject(mixed $data, DeserializationContext $context): ?object { if (empty($data['tagName']) && empty($data['tag_name'])) { throw new ObjectConstructionException('Tag name can not be empty'); diff --git a/src/Serializer/ObjectConstructor/TranslationObjectConstructor.php b/src/Serializer/ObjectConstructor/TranslationObjectConstructor.php index 40309d06..4bc72788 100644 --- a/src/Serializer/ObjectConstructor/TranslationObjectConstructor.php +++ b/src/Serializer/ObjectConstructor/TranslationObjectConstructor.php @@ -6,16 +6,17 @@ use JMS\Serializer\DeserializationContext; use JMS\Serializer\Exception\ObjectConstructionException; -use RZ\Roadiz\CoreBundle\Entity\Translation; +use RZ\Roadiz\Core\AbstractEntities\TranslationInterface; -class TranslationObjectConstructor extends AbstractTypedObjectConstructor +final class TranslationObjectConstructor extends AbstractTypedObjectConstructor { /** * @inheritDoc */ public function supports(string $className, array $data): bool { - return $className === Translation::class && array_key_exists('locale', $data); + return \is_subclass_of($className, TranslationInterface::class) && + array_key_exists('locale', $data); } /** @@ -28,13 +29,13 @@ protected function findObject($data, DeserializationContext $context): ?object } return $this->entityManager - ->getRepository(Translation::class) + ->getRepository(TranslationInterface::class) ->findOneByLocale($data['locale']); } protected function fillIdentifier(object $object, array $data): void { - if ($object instanceof Translation) { + if ($object instanceof TranslationInterface) { $object->setLocale($data['locale']); $object->setName($data['locale']); } diff --git a/src/Serializer/TranslationAwareContextBuilder.php b/src/Serializer/TranslationAwareContextBuilder.php index e12cc612..66867a64 100644 --- a/src/Serializer/TranslationAwareContextBuilder.php +++ b/src/Serializer/TranslationAwareContextBuilder.php @@ -4,7 +4,7 @@ namespace RZ\Roadiz\CoreBundle\Serializer; -use ApiPlatform\Core\Serializer\SerializerContextBuilderInterface; +use ApiPlatform\Serializer\SerializerContextBuilderInterface; use Doctrine\Persistence\ManagerRegistry; use RZ\Roadiz\Core\AbstractEntities\TranslationInterface; use RZ\Roadiz\CoreBundle\Preview\PreviewResolverInterface; @@ -13,18 +13,11 @@ final class TranslationAwareContextBuilder implements SerializerContextBuilderInterface { - private ManagerRegistry $managerRegistry; - private SerializerContextBuilderInterface $decorated; - private PreviewResolverInterface $previewResolver; - public function __construct( - SerializerContextBuilderInterface $decorated, - ManagerRegistry $managerRegistry, - PreviewResolverInterface $previewResolver + private readonly SerializerContextBuilderInterface $decorated, + private readonly ManagerRegistry $managerRegistry, + private readonly PreviewResolverInterface $previewResolver ) { - $this->decorated = $decorated; - $this->managerRegistry = $managerRegistry; - $this->previewResolver = $previewResolver; } /** * @inheritDoc @@ -33,25 +26,38 @@ public function createFromRequest(Request $request, bool $normalization, array $ { $context = $this->decorated->createFromRequest($request, $normalization, $extractedAttributes); - if (!isset($context['translation']) || !($context['translation'] instanceof TranslationInterface)) { - /** @var TranslationRepository $repository */ - $repository = $this->managerRegistry - ->getRepository(TranslationInterface::class); - - if ($this->previewResolver->isPreview()) { - $translation = $repository->findOneByLocaleOrOverrideLocale( - $request->query->get('_locale', $request->getLocale()) - ); - } else { - $translation = $repository->findOneAvailableByLocaleOrOverrideLocale( - $request->query->get('_locale', $request->getLocale()) - ); - } - - if ($translation instanceof TranslationInterface) { - $context['translation'] = $translation; - } + if (isset($context['translation']) && $context['translation'] instanceof TranslationInterface) { + return $context; + } + + /* + * Try to get translation resolved from LocaleSubscriber before + */ + $requestTranslation = $request->attributes->get('_translation'); + if ($requestTranslation instanceof TranslationInterface) { + $context['translation'] = $requestTranslation; + return $context; + } + + /** @var TranslationRepository $repository */ + $repository = $this->managerRegistry + ->getRepository(TranslationInterface::class); + $locale = $request->query->get('_locale', $request->getLocale()); + + if (!\is_string($locale)) { + return $context; } + + if ($this->previewResolver->isPreview()) { + $translation = $repository->findOneByLocaleOrOverrideLocale($locale); + } else { + $translation = $repository->findOneAvailableByLocaleOrOverrideLocale($locale); + } + + if ($translation instanceof TranslationInterface) { + $context['translation'] = $translation; + } + return $context; } } diff --git a/src/Test/NodeType/ApiResourceGeneratorTest.php b/src/Test/NodeType/ApiResourceGeneratorTest.php new file mode 100644 index 00000000..138a8efc --- /dev/null +++ b/src/Test/NodeType/ApiResourceGeneratorTest.php @@ -0,0 +1,152 @@ +getContainer()->get(ApiResourceOperationNameGenerator::class); + + return new ApiResourceGenerator( + $apiResourceOperationNameGenerator, + static::getGeneratedPath(), + new NullLogger(), + WebResponse::class + ); + } + + public function testGenerate(): void + { + $apiResourceGenerator = $this->getApiResourceGenerator(); + + $nodeType = new NodeType(); + $nodeType->setName('Test'); + + $apiResourceGenerator->generate($nodeType); + $resourcePath = $apiResourceGenerator->getResourcePath($nodeType); + $this->assertFileExists($resourcePath); + $this->assertFileEquals( + dirname(__DIR__) . '/../../tests/expected_api_resources/nstest.yml', + $resourcePath + ); + } + + public function testReachableGenerate(): void + { + $apiResourceGenerator = $this->getApiResourceGenerator(); + + $nodeType = new NodeType(); + $nodeType->setName('Test'); + $nodeType->setReachable(true); + + $apiResourceGenerator->generate($nodeType); + $resourcePath = $apiResourceGenerator->getResourcePath($nodeType); + $this->assertFileExists($resourcePath); + $this->assertFileExists(dirname(__DIR__) . '/../../tests/generated_api_resources/web_response.yml'); + $this->assertFileEquals( + dirname(__DIR__) . '/../../tests/expected_api_resources/nstest.yml', + $resourcePath + ); + $this->assertFileEquals( + dirname(__DIR__) . '/../../tests/expected_api_resources/web_response.yml', + dirname(__DIR__) . '/../../tests/generated_api_resources/web_response.yml', + ); + } + + public function testMultipleGenerate(): void + { + $apiResourceGenerator = $this->getApiResourceGenerator(); + + $nodeType = new NodeType(); + $nodeType->setName('Test'); + $nodeType->setReachable(true); + + $nodeType2 = new NodeType(); + $nodeType2->setName('SecondTest'); + $nodeType2->setReachable(true); + + $apiResourceGenerator->generate($nodeType); + $resourcePath = $apiResourceGenerator->getResourcePath($nodeType); + $this->assertFileExists($resourcePath); + $this->assertFileEquals( + dirname(__DIR__) . '/../../tests/expected_api_resources/nstest.yml', + $resourcePath + ); + + $apiResourceGenerator->generate($nodeType2); + $resourcePath2 = $apiResourceGenerator->getResourcePath($nodeType2); + $this->assertFileExists($resourcePath2); + $this->assertFileEquals( + dirname(__DIR__) . '/../../tests/expected_api_resources/nssecondtest.yml', + $resourcePath2 + ); + + $this->assertFileExists(dirname(__DIR__) . '/../../tests/generated_api_resources/web_response.yml'); + $this->assertFileEquals( + dirname(__DIR__) . '/../../tests/expected_api_resources/web_response_multiple.yml', + dirname(__DIR__) . '/../../tests/generated_api_resources/web_response.yml', + ); + } + + public function testRemoveGenerate(): void + { + $apiResourceGenerator = $this->getApiResourceGenerator(); + + $nodeType = new NodeType(); + $nodeType->setName('Test'); + $nodeType->setReachable(true); + + $nodeType2 = new NodeType(); + $nodeType2->setName('SecondTest'); + $nodeType2->setReachable(true); + + $apiResourceGenerator->generate($nodeType); + $apiResourceGenerator->generate($nodeType2); + + // Remove second node-type to check if operation + // is removed from web_response + $apiResourceGenerator->remove($nodeType2); + $resourcePath2 = $apiResourceGenerator->getResourcePath($nodeType2); + $this->assertFileDoesNotExist($resourcePath2); + + $this->assertFileExists(dirname(__DIR__) . '/../../tests/generated_api_resources/web_response.yml'); + $this->assertFileEquals( + dirname(__DIR__) . '/../../tests/expected_api_resources/web_response.yml', + dirname(__DIR__) . '/../../tests/generated_api_resources/web_response.yml', + ); + } + + protected function setUp(): void + { + parent::setUp(); + + $filesystem = new Filesystem(); + $filesystem->mkdir(static::getGeneratedPath()); + } + + + protected function tearDown(): void + { + parent::tearDown(); + + $filesystem = new Filesystem(); + $filesystem->remove(static::getGeneratedPath()); + } +} diff --git a/src/Traits/LoginRequestTrait.php b/src/Traits/LoginRequestTrait.php index d804a62d..19ea1b16 100644 --- a/src/Traits/LoginRequestTrait.php +++ b/src/Traits/LoginRequestTrait.php @@ -23,11 +23,11 @@ trait LoginRequestTrait abstract protected function getUserViewer(): UserViewer; /** - * @param FormInterface $form - * @param ObjectManager $entityManager - * @param LoggerInterface $logger + * @param FormInterface $form + * @param ObjectManager $entityManager + * @param LoggerInterface $logger * @param UrlGeneratorInterface $urlGenerator - * @param string $resetRoute + * @param string $resetRoute * * @return bool TRUE if confirmation has been sent. FALSE if errors * @throws \Doctrine\ORM\ORMException @@ -39,7 +39,7 @@ public function sendConfirmationEmail( LoggerInterface $logger, UrlGeneratorInterface $urlGenerator, string $resetRoute = 'loginResetPage' - ) { + ): bool { $email = $form->get('email')->getData(); /** @var User $user */ $user = $entityManager->getRepository(User::class)->findOneByEmail($email); @@ -52,10 +52,9 @@ public function sendConfirmationEmail( $user->setConfirmationToken($tokenGenerator->generateToken()); $entityManager->flush(); $userViewer = $this->getUserViewer(); - $userViewer->setUser($user); - $userViewer->sendPasswordResetLink($resetRoute); + $userViewer->sendPasswordResetLink($user, $resetRoute); return true; - } catch (\Exception $e) { + } catch (\Throwable $e) { $user->setPasswordRequestedAt(null); $user->setConfirmationToken(null); $entityManager->flush(); diff --git a/src/TwigExtension/AttributesExtension.php b/src/TwigExtension/AttributesExtension.php index 890cfa49..92755704 100644 --- a/src/TwigExtension/AttributesExtension.php +++ b/src/TwigExtension/AttributesExtension.php @@ -19,16 +19,10 @@ use Twig\TwigFunction; use Twig\TwigTest; -class AttributesExtension extends AbstractExtension +final class AttributesExtension extends AbstractExtension { - protected EntityManagerInterface $entityManager; - - /** - * @param EntityManagerInterface $entityManager - */ - public function __construct(EntityManagerInterface $entityManager) + public function __construct(private readonly EntityManagerInterface $entityManager) { - $this->entityManager = $entityManager; } public function getFunctions(): array @@ -213,7 +207,7 @@ public function getNodeSourceGroupedAttributeValues(?NodesSources $nodesSources, * * @return string|null */ - public function getAttributeLabelOrCode($mixed, TranslationInterface $translation = null): ?string + public function getAttributeLabelOrCode(mixed $mixed, TranslationInterface $translation = null): ?string { if (null === $mixed) { return null; @@ -240,7 +234,7 @@ public function getAttributeLabelOrCode($mixed, TranslationInterface $translatio * @param TranslationInterface|null $translation * @return string|null */ - public function getAttributeGroupLabelOrCode($mixed, TranslationInterface $translation = null): ?string + public function getAttributeGroupLabelOrCode(mixed $mixed, TranslationInterface $translation = null): ?string { if (null === $mixed) { return null; diff --git a/src/TwigExtension/BlockRenderExtension.php b/src/TwigExtension/BlockRenderExtension.php index e4944307..5e028743 100644 --- a/src/TwigExtension/BlockRenderExtension.php +++ b/src/TwigExtension/BlockRenderExtension.php @@ -15,16 +15,10 @@ * Extension that allow render inner page part calling directly their * controller response instead of doing a simple include. */ -class BlockRenderExtension extends AbstractExtension +final class BlockRenderExtension extends AbstractExtension { - protected FragmentHandler $handler; - - /** - * @param FragmentHandler $handler - */ - public function __construct(FragmentHandler $handler) + public function __construct(private readonly FragmentHandler $handler) { - $this->handler = $handler; } public function getFilters(): array @@ -42,7 +36,7 @@ public function getFilters(): array * @return string * @throws RuntimeError */ - public function blockRender(NodesSources $nodeSource = null, string $themeName = "DefaultTheme", array $assignation = []) + public function blockRender(NodesSources $nodeSource = null, string $themeName = "DefaultTheme", array $assignation = []): string { if (null !== $nodeSource) { if (!empty($themeName)) { diff --git a/src/TwigExtension/CentralTruncateExtension.php b/src/TwigExtension/CentralTruncateExtension.php index 8c3c8851..077e73e9 100644 --- a/src/TwigExtension/CentralTruncateExtension.php +++ b/src/TwigExtension/CentralTruncateExtension.php @@ -9,7 +9,7 @@ use function Symfony\Component\String\u; -class CentralTruncateExtension extends AbstractExtension +final class CentralTruncateExtension extends AbstractExtension { public function getFilters(): array { diff --git a/src/TwigExtension/DocumentUrlExtension.php b/src/TwigExtension/DocumentUrlExtension.php index 945ffda6..af68dd7d 100644 --- a/src/TwigExtension/DocumentUrlExtension.php +++ b/src/TwigExtension/DocumentUrlExtension.php @@ -15,21 +15,12 @@ /** * Extension that allow render documents Url */ -class DocumentUrlExtension extends AbstractExtension +final class DocumentUrlExtension extends AbstractExtension { - protected DocumentUrlGeneratorInterface $documentUrlGenerator; - protected bool $throwExceptions; - - /** - * @param DocumentUrlGeneratorInterface $documentUrlGenerator - * @param bool $throwExceptions Trigger exception if using filter on NULL values (default: false) - */ public function __construct( - DocumentUrlGeneratorInterface $documentUrlGenerator, - bool $throwExceptions = false + private readonly DocumentUrlGeneratorInterface $documentUrlGenerator, + private readonly bool $throwExceptions = false ) { - $this->throwExceptions = $throwExceptions; - $this->documentUrlGenerator = $documentUrlGenerator; } /** @@ -54,7 +45,7 @@ public function getFilters(): array * @return string * @throws RuntimeError */ - public function getUrl(PersistableInterface $mixed = null, array $criteria = []) + public function getUrl(PersistableInterface $mixed = null, array $criteria = []): string { if (null === $mixed) { if ($this->throwExceptions) { diff --git a/src/TwigExtension/HandlerExtension.php b/src/TwigExtension/HandlerExtension.php index 186355d6..46bd6cd0 100644 --- a/src/TwigExtension/HandlerExtension.php +++ b/src/TwigExtension/HandlerExtension.php @@ -4,6 +4,8 @@ namespace RZ\Roadiz\CoreBundle\TwigExtension; +use Psr\Container\ContainerExceptionInterface; +use Psr\Container\NotFoundExceptionInterface; use RZ\Roadiz\Core\AbstractEntities\AbstractEntity; use RZ\Roadiz\Core\Handlers\AbstractHandler; use RZ\Roadiz\CoreBundle\EntityHandler\HandlerFactory; @@ -13,14 +15,8 @@ final class HandlerExtension extends AbstractExtension { - private HandlerFactory $handlerFactory; - - /** - * @param HandlerFactory $handlerFactory - */ - public function __construct(HandlerFactory $handlerFactory) + public function __construct(private readonly HandlerFactory $handlerFactory) { - $this->handlerFactory = $handlerFactory; } public function getFilters(): array @@ -34,8 +30,10 @@ public function getFilters(): array * @param mixed $mixed * @return AbstractHandler|null * @throws RuntimeError + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface */ - public function getHandler($mixed) + public function getHandler(mixed $mixed): ?AbstractHandler { if (null === $mixed) { return null; diff --git a/src/TwigExtension/JwtExtension.php b/src/TwigExtension/JwtExtension.php index c8d2452b..246e20d8 100644 --- a/src/TwigExtension/JwtExtension.php +++ b/src/TwigExtension/JwtExtension.php @@ -9,24 +9,16 @@ use Psr\Log\LoggerInterface; use RZ\Roadiz\CoreBundle\Preview\User\PreviewUserProviderInterface; use Symfony\Component\Security\Core\Exception\AccessDeniedException; -use Symfony\Component\Security\Core\User\UserInterface; use Twig\Extension\AbstractExtension; use Twig\TwigFunction; final class JwtExtension extends AbstractExtension { - private PreviewUserProviderInterface $previewUserProvider; - private JWTTokenManagerInterface $tokenManager; - private LoggerInterface $logger; - public function __construct( - JWTTokenManagerInterface $tokenManager, - LoggerInterface $logger, - PreviewUserProviderInterface $previewUserProvider + private readonly JWTTokenManagerInterface $tokenManager, + private readonly LoggerInterface $logger, + private readonly PreviewUserProviderInterface $previewUserProvider ) { - $this->tokenManager = $tokenManager; - $this->logger = $logger; - $this->previewUserProvider = $previewUserProvider; } public function getFunctions(): array diff --git a/src/TwigExtension/LogExtension.php b/src/TwigExtension/LogExtension.php new file mode 100644 index 00000000..cfaa612e --- /dev/null +++ b/src/TwigExtension/LogExtension.php @@ -0,0 +1,114 @@ + [$this, 'isUrlGenerationSafe']]), + ]; + } + + public function getEditPath(?object $log): ?string + { + if (!($log instanceof Log) || null === $log->getEntityId()) { + return null; + } + + switch ($log->getEntityClass()) { + case Node::class: + case NodesSources::class: + if ( + $this->security->isGranted('ROLE_ACCESS_NODES') && + isset($log->getAdditionalData()['node_id']) && + isset($log->getAdditionalData()['translation_id']) + ) { + return $this->urlGenerator->generate('nodesEditSourcePage', [ + 'nodeId' => $log->getAdditionalData()['node_id'], + 'translationId' => $log->getAdditionalData()['translation_id'], + ]); + } + break; + case Tag::class: + if ( + $this->security->isGranted('ROLE_ACCESS_TAGS') + ) { + return $this->urlGenerator->generate('tagsEditPage', [ + 'tagId' => $log->getEntityId(), + ]); + } + break; + case Document::class: + if ( + $this->security->isGranted('ROLE_ACCESS_DOCUMENTS') + ) { + return $this->urlGenerator->generate('documentsEditPage', [ + 'documentId' => $log->getEntityId(), + ]); + } + break; + case User::class: + if ( + $this->security->isGranted('ROLE_ACCESS_USERS') + ) { + return $this->urlGenerator->generate('usersEditPage', [ + 'id' => $log->getEntityId(), + ]); + } + break; + case CustomForm::class: + if ( + $this->security->isGranted('ROLE_ACCESS_CUSTOMFORMS') + ) { + return $this->urlGenerator->generate('customFormsEditPage', [ + 'id' => $log->getEntityId(), + ]); + } + break; + case Translation::class: + if ( + $this->security->isGranted('ROLE_ACCESS_TRANSLATIONS') + ) { + return $this->urlGenerator->generate('translationsEditPage', [ + 'translationId' => $log->getEntityId(), + ]); + } + break; + case Setting::class: + if ( + $this->security->isGranted('ROLE_ACCESS_SETTINGS') + ) { + return $this->urlGenerator->generate('settingsEditPage', [ + 'settingId' => $log->getEntityId(), + ]); + } + break; + } + + return null; + } +} diff --git a/src/TwigExtension/NodesSourcesExtension.php b/src/TwigExtension/NodesSourcesExtension.php index 59a2b90e..55d8fcc8 100644 --- a/src/TwigExtension/NodesSourcesExtension.php +++ b/src/TwigExtension/NodesSourcesExtension.php @@ -4,11 +4,13 @@ namespace RZ\Roadiz\CoreBundle\TwigExtension; +use Doctrine\ORM\NonUniqueResultException; +use Doctrine\Persistence\ManagerRegistry; +use Psr\Container\ContainerExceptionInterface; +use Psr\Container\NotFoundExceptionInterface; use RZ\Roadiz\CoreBundle\Bag\NodeTypes; use RZ\Roadiz\CoreBundle\Entity\NodesSources; -use RZ\Roadiz\CoreBundle\EntityApi\NodeSourceApi; -use RZ\Roadiz\CoreBundle\EntityHandler\HandlerFactory; -use RZ\Roadiz\CoreBundle\EntityHandler\NodesSourcesHandler; +use RZ\Roadiz\CoreBundle\Entity\Tag; use Twig\Error\RuntimeError; use Twig\Extension\AbstractExtension; use Twig\TwigFilter; @@ -19,27 +21,11 @@ */ final class NodesSourcesExtension extends AbstractExtension { - protected NodeSourceApi $nodeSourceApi; - protected HandlerFactory $handlerFactory; - private bool $throwExceptions; - private NodeTypes $nodeTypesBag; - - /** - * @param NodeSourceApi $nodeSourceApi - * @param HandlerFactory $handlerFactory - * @param NodeTypes $nodeTypesBag - * @param bool $throwExceptions - */ public function __construct( - NodeSourceApi $nodeSourceApi, - HandlerFactory $handlerFactory, - NodeTypes $nodeTypesBag, - bool $throwExceptions = false + private readonly ManagerRegistry $managerRegistry, + private readonly NodeTypes $nodeTypesBag, + private readonly bool $throwExceptions = false ) { - $this->throwExceptions = $throwExceptions; - $this->handlerFactory = $handlerFactory; - $this->nodeTypesBag = $nodeTypesBag; - $this->nodeSourceApi = $nodeSourceApi; } public function getFilters(): array @@ -76,10 +62,10 @@ public function getTests(): array * @param NodesSources|null $ns * @param array|null $criteria * @param array|null $order - * @return array + * @return iterable * @throws RuntimeError */ - public function getChildren(NodesSources $ns = null, array $criteria = null, array $order = null) + public function getChildren(NodesSources $ns = null, array $criteria = null, array $order = null): iterable { if (null === $ns) { if ($this->throwExceptions) { @@ -88,24 +74,10 @@ public function getChildren(NodesSources $ns = null, array $criteria = null, arr return []; } } - $defaultCrit = [ - 'node.parent' => $ns->getNode(), - 'translation' => $ns->getTranslation(), - ]; - - if (null !== $order) { - $defaultOrder = $order; - } else { - $defaultOrder = [ - 'node.position' => 'ASC', - ]; - } - - if (null !== $criteria) { - $defaultCrit = array_merge($defaultCrit, $criteria); - } - return $this->nodeSourceApi->getBy($defaultCrit, $defaultOrder); + return $this->managerRegistry + ->getRepository(NodesSources::class) + ->findChildren($ns, $criteria, $order); } /** @@ -115,7 +87,7 @@ public function getChildren(NodesSources $ns = null, array $criteria = null, arr * @return NodesSources|null * @throws RuntimeError */ - public function getNext(NodesSources $ns = null, array $criteria = null, array $order = null) + public function getNext(NodesSources $ns = null, array $criteria = null, array $order = null): ?NodesSources { if (null === $ns) { if ($this->throwExceptions) { @@ -124,9 +96,10 @@ public function getNext(NodesSources $ns = null, array $criteria = null, array $ return null; } } - /** @var NodesSourcesHandler $nodeSourceHandler */ - $nodeSourceHandler = $this->handlerFactory->getHandler($ns); - return $nodeSourceHandler->getNext($criteria, $order); + + return $this->managerRegistry + ->getRepository(NodesSources::class) + ->findNext($ns, $criteria, $order); } /** @@ -136,7 +109,7 @@ public function getNext(NodesSources $ns = null, array $criteria = null, array $ * @return NodesSources|null * @throws RuntimeError */ - public function getPrevious(NodesSources $ns = null, array $criteria = null, array $order = null) + public function getPrevious(NodesSources $ns = null, array $criteria = null, array $order = null): ?NodesSources { if (null === $ns) { if ($this->throwExceptions) { @@ -145,9 +118,10 @@ public function getPrevious(NodesSources $ns = null, array $criteria = null, arr return null; } } - /** @var NodesSourcesHandler $nodeSourceHandler */ - $nodeSourceHandler = $this->handlerFactory->getHandler($ns); - return $nodeSourceHandler->getPrevious($criteria, $order); + + return $this->managerRegistry + ->getRepository(NodesSources::class) + ->findPrevious($ns, $criteria, $order); } /** @@ -157,7 +131,7 @@ public function getPrevious(NodesSources $ns = null, array $criteria = null, arr * @return NodesSources|null * @throws RuntimeError */ - public function getLastSibling(NodesSources $ns = null, array $criteria = null, array $order = null) + public function getLastSibling(NodesSources $ns = null, array $criteria = null, array $order = null): ?NodesSources { if (null === $ns) { if ($this->throwExceptions) { @@ -166,9 +140,10 @@ public function getLastSibling(NodesSources $ns = null, array $criteria = null, return null; } } - /** @var NodesSourcesHandler $nodeSourceHandler */ - $nodeSourceHandler = $this->handlerFactory->getHandler($ns); - return $nodeSourceHandler->getLastSibling($criteria, $order); + + return $this->managerRegistry + ->getRepository(NodesSources::class) + ->findLastSibling($ns, $criteria, $order); } /** @@ -178,7 +153,7 @@ public function getLastSibling(NodesSources $ns = null, array $criteria = null, * @return NodesSources|null * @throws RuntimeError */ - public function getFirstSibling(NodesSources $ns = null, array $criteria = null, array $order = null) + public function getFirstSibling(NodesSources $ns = null, array $criteria = null, array $order = null): ?NodesSources { if (null === $ns) { if ($this->throwExceptions) { @@ -187,9 +162,10 @@ public function getFirstSibling(NodesSources $ns = null, array $criteria = null, return null; } } - /** @var NodesSourcesHandler $nodeSourceHandler */ - $nodeSourceHandler = $this->handlerFactory->getHandler($ns); - return $nodeSourceHandler->getFirstSibling($criteria, $order); + + return $this->managerRegistry + ->getRepository(NodesSources::class) + ->findFirstSibling($ns, $criteria, $order); } /** @@ -197,7 +173,7 @@ public function getFirstSibling(NodesSources $ns = null, array $criteria = null, * @return NodesSources|null * @throws RuntimeError */ - public function getParent(NodesSources $ns = null) + public function getParent(NodesSources $ns = null): ?NodesSources { if (null === $ns) { if ($this->throwExceptions) { @@ -214,9 +190,12 @@ public function getParent(NodesSources $ns = null) * @param NodesSources|null $ns * @param array|null $criteria * @return array + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface * @throws RuntimeError + * @throws NonUniqueResultException */ - public function getParents(NodesSources $ns = null, array $criteria = null) + public function getParents(NodesSources $ns = null, array $criteria = null): array { if (null === $ns) { if ($this->throwExceptions) { @@ -225,17 +204,20 @@ public function getParents(NodesSources $ns = null, array $criteria = null) return []; } } - /** @var NodesSourcesHandler $nodeSourceHandler */ - $nodeSourceHandler = $this->handlerFactory->getHandler($ns); - return $nodeSourceHandler->getParents($criteria); + + return $this->managerRegistry + ->getRepository(NodesSources::class) + ->findParents($ns, $criteria); } /** * @param NodesSources|null $ns - * @return array + * @return iterable * @throws RuntimeError + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface */ - public function getTags(NodesSources $ns = null) + public function getTags(NodesSources $ns = null): iterable { if (null === $ns) { if ($this->throwExceptions) { @@ -244,8 +226,9 @@ public function getTags(NodesSources $ns = null) return []; } } - /** @var NodesSourcesHandler $nodeSourceHandler */ - $nodeSourceHandler = $this->handlerFactory->getHandler($ns); - return $nodeSourceHandler->getTags(); + + return $this->managerRegistry + ->getRepository(Tag::class) + ->findByNodesSources($ns); } } diff --git a/src/TwigExtension/RoadizExtension.php b/src/TwigExtension/RoadizExtension.php index 62b7fd10..c5d4d333 100644 --- a/src/TwigExtension/RoadizExtension.php +++ b/src/TwigExtension/RoadizExtension.php @@ -11,35 +11,18 @@ use Twig\Extension\AbstractExtension; use Twig\Extension\GlobalsInterface; -class RoadizExtension extends AbstractExtension implements GlobalsInterface +final class RoadizExtension extends AbstractExtension implements GlobalsInterface { - protected Settings $settingsBag; - protected NodeTypes $nodeTypesBag; - protected PreviewResolverInterface $previewResolver; - protected NodeChrootResolver $chrootResolver; - protected string $cmsVersion; - protected string $cmsVersionPrefix; - protected bool $hideRoadizVersion; - protected int $maxVersionsShowed; - public function __construct( - Settings $settingsBag, - NodeTypes $nodeTypesBag, - PreviewResolverInterface $previewResolver, - NodeChrootResolver $chrootResolver, - string $cmsVersion, - string $cmsVersionPrefix, - bool $hideRoadizVersion, - int $maxVersionsShowed + private readonly Settings $settingsBag, + private readonly NodeTypes $nodeTypesBag, + private readonly PreviewResolverInterface $previewResolver, + private readonly NodeChrootResolver $chrootResolver, + private readonly string $cmsVersion, + private readonly string $cmsVersionPrefix, + private readonly bool $hideRoadizVersion, + private readonly int $maxVersionsShowed ) { - $this->settingsBag = $settingsBag; - $this->nodeTypesBag = $nodeTypesBag; - $this->previewResolver = $previewResolver; - $this->chrootResolver = $chrootResolver; - $this->cmsVersion = $cmsVersion; - $this->cmsVersionPrefix = $cmsVersionPrefix; - $this->hideRoadizVersion = $hideRoadizVersion; - $this->maxVersionsShowed = $maxVersionsShowed; } /** diff --git a/src/TwigExtension/RoutingExtension.php b/src/TwigExtension/RoutingExtension.php index bfdd578d..f78d3dd3 100644 --- a/src/TwigExtension/RoutingExtension.php +++ b/src/TwigExtension/RoutingExtension.php @@ -14,21 +14,12 @@ /** * Override Symfony RoutingExtension to support object url generation. */ -class RoutingExtension extends AbstractExtension +final class RoutingExtension extends AbstractExtension { - private UrlGeneratorInterface $generator; - private \Symfony\Bridge\Twig\Extension\RoutingExtension $decorated; - - /** - * @param UrlGeneratorInterface $generator - * @param \Symfony\Bridge\Twig\Extension\RoutingExtension $decorated - */ public function __construct( - \Symfony\Bridge\Twig\Extension\RoutingExtension $decorated, - UrlGeneratorInterface $generator + private readonly \Symfony\Bridge\Twig\Extension\RoutingExtension $decorated, + private readonly UrlGeneratorInterface $generator ) { - $this->generator = $generator; - $this->decorated = $decorated; } /** @@ -49,7 +40,7 @@ public function getFunctions(): array * @return string * @throws RuntimeError */ - public function getPath($name, array $parameters = [], bool $relative = false): string + public function getPath(string|object|null $name, array $parameters = [], bool $relative = false): string { if (is_string($name)) { return $this->decorated->getPath( @@ -75,7 +66,7 @@ public function getPath($name, array $parameters = [], bool $relative = false): * @return string * @throws RuntimeError */ - public function getUrl($name, array $parameters = [], bool $schemeRelative = false): string + public function getUrl(string|object|null $name, array $parameters = [], bool $schemeRelative = false): string { if (is_string($name)) { return $this->decorated->getUrl( diff --git a/src/TwigExtension/TransChoiceExtension.php b/src/TwigExtension/TransChoiceExtension.php index 949b0243..96c19b15 100644 --- a/src/TwigExtension/TransChoiceExtension.php +++ b/src/TwigExtension/TransChoiceExtension.php @@ -15,14 +15,8 @@ */ final class TransChoiceExtension extends AbstractExtension { - private TranslatorInterface $translator; - - /** - * @param TranslatorInterface $translator - */ - public function __construct(TranslatorInterface $translator) + public function __construct(private readonly TranslatorInterface $translator) { - $this->translator = $translator; } public function getFilters(): array diff --git a/src/TwigExtension/TranslationExtension.php b/src/TwigExtension/TranslationExtension.php index 807a2fc7..cd248352 100644 --- a/src/TwigExtension/TranslationExtension.php +++ b/src/TwigExtension/TranslationExtension.php @@ -34,7 +34,7 @@ public function getTests(): array * * @return bool */ - public function isLocaleRtl($mixed) + public function isLocaleRtl(mixed $mixed): bool { if ($mixed instanceof TranslationInterface) { return $mixed->isRtl(); diff --git a/src/TwigExtension/TranslationMenuExtension.php b/src/TwigExtension/TranslationMenuExtension.php index b1fdb2fd..2c0bccd8 100644 --- a/src/TwigExtension/TranslationMenuExtension.php +++ b/src/TwigExtension/TranslationMenuExtension.php @@ -13,17 +13,10 @@ final class TranslationMenuExtension extends AbstractExtension { - private RequestStack $requestStack; - private TranslationViewer $translationViewer; - - /** - * @param RequestStack $requestStack - * @param TranslationViewer $translationViewer - */ - public function __construct(RequestStack $requestStack, TranslationViewer $translationViewer) - { - $this->requestStack = $requestStack; - $this->translationViewer = $translationViewer; + public function __construct( + private readonly RequestStack $requestStack, + private readonly TranslationViewer $translationViewer + ) { } public function getFilters(): array @@ -40,7 +33,7 @@ public function getFilters(): array * @return array * @throws ORMException */ - public function getMenuAssignation(TranslationInterface $translation = null, bool $absolute = false) + public function getMenuAssignation(TranslationInterface $translation = null, bool $absolute = false): array { if (null !== $translation) { $this->translationViewer->setTranslation($translation); diff --git a/src/Workflow/Event/NodeStatusGuardListener.php b/src/Workflow/Event/NodeStatusGuardListener.php index b9550d93..edc6b66e 100644 --- a/src/Workflow/Event/NodeStatusGuardListener.php +++ b/src/Workflow/Event/NodeStatusGuardListener.php @@ -5,8 +5,9 @@ namespace RZ\Roadiz\CoreBundle\Workflow\Event; use RZ\Roadiz\CoreBundle\Entity\Node; +use RZ\Roadiz\CoreBundle\Security\Authorization\Voter\NodeVoter; use Symfony\Component\EventDispatcher\EventSubscriberInterface; -use Symfony\Component\Security\Core\Security; +use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\Workflow\Event\GuardEvent; use Symfony\Component\Workflow\TransitionBlocker; @@ -37,7 +38,7 @@ public static function getSubscribedEvents(): array public function guard(GuardEvent $event): void { - if (!$this->security->isGranted('ROLE_ACCESS_NODES')) { + if (!$this->security->isGranted(NodeVoter::EDIT_CONTENT, $event->getSubject())) { $event->addTransitionBlocker(new TransitionBlocker( 'User is not allowed to edit this node.', '1' @@ -47,7 +48,7 @@ public function guard(GuardEvent $event): void public function guardPublish(GuardEvent $event): void { - if (!$this->security->isGranted('ROLE_ACCESS_NODES_STATUS')) { + if (!$this->security->isGranted(NodeVoter::EDIT_STATUS, $event->getSubject())) { $event->addTransitionBlocker(new TransitionBlocker( 'User is not allowed to publish this node.', '1' @@ -65,7 +66,7 @@ public function guardArchive(GuardEvent $event): void '1' )); } - if (!$this->security->isGranted('ROLE_ACCESS_NODES_STATUS')) { + if (!$this->security->isGranted(NodeVoter::EDIT_STATUS, $event->getSubject())) { $event->addTransitionBlocker(new TransitionBlocker( 'User is not allowed to archive this node.', '1' @@ -83,7 +84,7 @@ public function guardDelete(GuardEvent $event): void '1' )); } - if (!$this->security->isGranted('ROLE_ACCESS_NODES_DELETE')) { + if (!$this->security->isGranted(NodeVoter::DELETE, $event->getSubject())) { $event->addTransitionBlocker(new TransitionBlocker( 'User is not allowed to delete this node.', '1' diff --git a/src/Xlsx/NodeSourceXlsxSerializer.php b/src/Xlsx/NodeSourceXlsxSerializer.php index b2579415..56e19597 100644 --- a/src/Xlsx/NodeSourceXlsxSerializer.php +++ b/src/Xlsx/NodeSourceXlsxSerializer.php @@ -68,7 +68,6 @@ public function toArray($nodeSource): array $data['title'] = $nodeSource->getTitle(); $data['published_at'] = $nodeSource->getPublishedAt(); $data['meta_title'] = $nodeSource->getMetaTitle(); - $data['meta_keywords'] = $nodeSource->getMetaKeywords(); $data['meta_description'] = $nodeSource->getMetaDescription(); $data = array_merge($data, $this->getSourceFields($nodeSource)); diff --git a/src/Xlsx/XlsxExporter.php b/src/Xlsx/XlsxExporter.php index 20988aa7..633d7017 100644 --- a/src/Xlsx/XlsxExporter.php +++ b/src/Xlsx/XlsxExporter.php @@ -85,7 +85,10 @@ public function exportXlsx($data, $keys = []) foreach ($headerkeys as $key => $value) { $columnAlpha = Coordinate::stringFromColumnIndex($key + 1); $activeSheet->getStyle($columnAlpha . $activeRow)->applyFromArray($headerStyles); - $activeSheet->setCellValueByColumnAndRow($key + 1, $activeRow, $this->translator->trans($value)); + if (\is_string($value)) { + $value = $this->translator->trans($value); + } + $activeSheet->setCellValueByColumnAndRow($key + 1, $activeRow, $value); } $activeRow++; } @@ -104,7 +107,7 @@ public function exportXlsx($data, $keys = []) continue; } - if ($value instanceof \DateTime) { + if ($value instanceof \DateTimeInterface) { $value = Date::PHPToExcel($value); $activeSheet->getStyle($columnAlpha . ($activeRow)) ->getNumberFormat() @@ -134,6 +137,11 @@ public function exportXlsx($data, $keys = []) $writer = new Xlsx($spreadsheet); ob_start(); $writer->save('php://output'); - return ob_get_clean(); + $output = ob_get_clean(); + + if (!\is_string($output)) { + throw new \RuntimeException('Output is not a string.'); + } + return $output; } } diff --git a/templates/DataCollector/solarium.html.twig b/templates/DataCollector/solarium.html.twig new file mode 100644 index 00000000..5a26a650 --- /dev/null +++ b/templates/DataCollector/solarium.html.twig @@ -0,0 +1,81 @@ +{# @see https://github.com/nelmio/NelmioSolariumBundle #} +{% extends '@WebProfiler/Profiler/layout.html.twig' %} + +{% block toolbar %} + {% set icon %} + {{ include('@RoadizCore/DataCollector/solr.svg') }} + {{ collector.querycount }} + + in + {{ '%0.2f'|format(collector.totaltime * 1000) }} + ms + + {% endset %} + {% set text %} +
+ Solr Queries + {{ collector.querycount }} +
+
+ Query time + {{ '%0.2f'|format(collector.totaltime * 1000) }} ms +
+ {% endset %} + {% include '@WebProfiler/Profiler/toolbar_item.html.twig' with { 'link': true } %} +{% endblock %} + +{% block menu %} + + {{ include('@RoadizCore/DataCollector/solr.svg') }} + Solr + +{% endblock %} + +{% block panel %} + {% if collector.queries is empty %} +

+ No queries. +

+ {% else %} +
    + {% for i, query in collector.queries %} +
  • +

    Request {{ loop.index }} ({{ query.request.uri }})

    +
    +

    Params

    + + + + + + + + + {% for key, value in query.request.params %} + + + {% if value is iterable %} + + {% else %} + + {% endif %} + + {% endfor %} + +
    KeyValue
    {{ key }}{{ value|join('
    ')|raw }}
    {{ value }}
    + +

    Response

    + + {% if query.response %} + HTTP-Result: {{ query.response.statuscode }} ({{ '%0.2f'|format(query.duration * 1000) }} ms)
    + {{ query.response.body }} + {% else %} + Request failed, no response logged + {% endif %} +
    +
    +
  • + {% endfor %} +
+ {% endif %} +{% endblock %} diff --git a/templates/DataCollector/solr.svg b/templates/DataCollector/solr.svg new file mode 100644 index 00000000..2f16b911 --- /dev/null +++ b/templates/DataCollector/solr.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/templates/customForm/customForm.html.twig b/templates/customForm/customForm.html.twig index 445238db..86d9b857 100644 --- a/templates/customForm/customForm.html.twig +++ b/templates/customForm/customForm.html.twig @@ -16,7 +16,7 @@ {{ form_widget(form) }}
{% apply spaceless %} - diff --git a/templates/email/forms/answerForm.html.twig b/templates/email/forms/answerForm.html.twig index 2fc13c63..a6cf6428 100644 --- a/templates/email/forms/answerForm.html.twig +++ b/templates/email/forms/answerForm.html.twig @@ -3,6 +3,9 @@ {% block title %}{{ title }}{% endblock %} {% block content_table %} +{% if requestLocale is not defined %} + {% set requestLocale = app.request.locale %} +{% endif %}
@@ -22,10 +25,10 @@ {% for field in fields %} - +
{{ field.label|default(field.name|trans) }}{{ field.label|default(field.name|trans(locale=requestLocale)) }} {% if field.name == "submittedAt" %} - {{ field.value|format_datetime("medium", "short", locale=app.request.locale) }} + {{ field.value|format_datetime("medium", "short", locale=requestLocale) }} {% else %} {{ field.value }} {% endif %} diff --git a/templates/email/forms/answerForm.txt.twig b/templates/email/forms/answerForm.txt.twig index 762450bc..3d9eba19 100644 --- a/templates/email/forms/answerForm.txt.twig +++ b/templates/email/forms/answerForm.txt.twig @@ -3,11 +3,15 @@ {% block title %}{{ title }}{% endblock %} {% block content_table %} +{% if requestLocale is not defined %} + {% set requestLocale = app.request.locale %} +{% endif %} {% trans %}answer.form{% endtrans %} --- {% for field in fields %} -{{ field.label|default(field.name|trans) }}: {% if field.name == "submittedAt" %}{{ field.value|format_datetime("medium", "short", locale=app.request.locale) }}{% else %}{{ field.value }}{% endif %} + +{{ field.label|default(field.name|trans(locale=requestLocale)) }}: {% if field.name == "submittedAt" %}{{ field.value|format_datetime("medium", "short", locale=requestLocale) }}{% else %}{{ field.value }}{% endif %} {% endfor %} {% endblock %} diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 00000000..469dccee --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,11 @@ +bootEnv(dirname(__DIR__).'/.env'); +} diff --git a/tests/expected_api_resources/nssecondtest.yml b/tests/expected_api_resources/nssecondtest.yml new file mode 100644 index 00000000..50af807a --- /dev/null +++ b/tests/expected_api_resources/nssecondtest.yml @@ -0,0 +1,35 @@ +resources: + App\GeneratedEntity\NSSecondTest: + shortName: SecondTest + types: + - SecondTest + operations: + secondtest_get_collection: + method: GET + class: ApiPlatform\Metadata\GetCollection + shortName: SecondTest + normalizationContext: + enable_max_depth: true + groups: + - nodes_sources_base + - nodes_sources_default + - urls + - tag_base + - translation_base + - document_display + - document_thumbnails + - document_display_sources + secondtest_get: + method: GET + class: ApiPlatform\Metadata\Get + shortName: SecondTest + normalizationContext: + groups: + - nodes_sources + - node_listing + - urls + - tag_base + - translation_base + - document_display + - document_thumbnails + - document_display_sources diff --git a/tests/expected_api_resources/nstest.yml b/tests/expected_api_resources/nstest.yml new file mode 100644 index 00000000..cc8c952f --- /dev/null +++ b/tests/expected_api_resources/nstest.yml @@ -0,0 +1,35 @@ +resources: + App\GeneratedEntity\NSTest: + shortName: Test + types: + - Test + operations: + test_get_collection: + method: GET + class: ApiPlatform\Metadata\GetCollection + shortName: Test + normalizationContext: + enable_max_depth: true + groups: + - nodes_sources_base + - nodes_sources_default + - urls + - tag_base + - translation_base + - document_display + - document_thumbnails + - document_display_sources + test_get: + method: GET + class: ApiPlatform\Metadata\Get + shortName: Test + normalizationContext: + groups: + - nodes_sources + - node_listing + - urls + - tag_base + - translation_base + - document_display + - document_thumbnails + - document_display_sources diff --git a/tests/expected_api_resources/web_response.yml b/tests/expected_api_resources/web_response.yml new file mode 100644 index 00000000..176c0a5d --- /dev/null +++ b/tests/expected_api_resources/web_response.yml @@ -0,0 +1,32 @@ +resources: + RZ\Roadiz\CoreBundle\Api\Model\WebResponse: + operations: + test_get_by_path: + method: GET + class: ApiPlatform\Metadata\Get + uriTemplate: /web_response_by_path + read: false + controller: RZ\Roadiz\CoreBundle\Api\Controller\GetWebResponseByPathController + normalizationContext: + pagination_enabled: false + enable_max_depth: true + groups: + - test_get_by_path + - nodes_sources + - node_listing + - urls + - tag_base + - translation_base + - document_display + - document_thumbnails + - document_display_sources + - web_response + - walker + - children + openapiContext: + tags: + - WebResponse + summary: 'Get a Test by its path wrapped in a WebResponse object' + description: 'Get a Test by its path wrapped in a WebResponse' + parameters: + - { type: string, name: path, in: query, required: true, description: 'Resource path, or `/` for home page', schema: { type: string } } diff --git a/tests/expected_api_resources/web_response_multiple.yml b/tests/expected_api_resources/web_response_multiple.yml new file mode 100644 index 00000000..eff72beb --- /dev/null +++ b/tests/expected_api_resources/web_response_multiple.yml @@ -0,0 +1,61 @@ +resources: + RZ\Roadiz\CoreBundle\Api\Model\WebResponse: + operations: + test_get_by_path: + method: GET + class: ApiPlatform\Metadata\Get + uriTemplate: /web_response_by_path + read: false + controller: RZ\Roadiz\CoreBundle\Api\Controller\GetWebResponseByPathController + normalizationContext: + pagination_enabled: false + enable_max_depth: true + groups: + - test_get_by_path + - nodes_sources + - node_listing + - urls + - tag_base + - translation_base + - document_display + - document_thumbnails + - document_display_sources + - web_response + - walker + - children + openapiContext: + tags: + - WebResponse + summary: 'Get a Test by its path wrapped in a WebResponse object' + description: 'Get a Test by its path wrapped in a WebResponse' + parameters: + - { type: string, name: path, in: query, required: true, description: 'Resource path, or `/` for home page', schema: { type: string } } + secondtest_get_by_path: + method: GET + class: ApiPlatform\Metadata\Get + uriTemplate: /web_response_by_path + read: false + controller: RZ\Roadiz\CoreBundle\Api\Controller\GetWebResponseByPathController + normalizationContext: + pagination_enabled: false + enable_max_depth: true + groups: + - secondtest_get_by_path + - nodes_sources + - node_listing + - urls + - tag_base + - translation_base + - document_display + - document_thumbnails + - document_display_sources + - web_response + - walker + - children + openapiContext: + tags: + - WebResponse + summary: 'Get a SecondTest by its path wrapped in a WebResponse object' + description: 'Get a SecondTest by its path wrapped in a WebResponse' + parameters: + - { type: string, name: path, in: query, required: true, description: 'Resource path, or `/` for home page', schema: { type: string } } diff --git a/tests/object-manager.php b/tests/object-manager.php new file mode 100644 index 00000000..c15dbe95 --- /dev/null +++ b/tests/object-manager.php @@ -0,0 +1,11 @@ +bootEnv(__DIR__ . '/../.env'); + +$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']); +$kernel->boot(); +return $kernel->getContainer()->get('doctrine')->getManager(); diff --git a/translations/core/messages.en.xlf b/translations/core/messages.en.xlf index 186ea829..760ae856 100644 --- a/translations/core/messages.en.xlf +++ b/translations/core/messages.en.xlf @@ -157,6 +157,24 @@ password_should_be_at_least_{{length}}_characters_long Password should be at least {{length}} characters long + + attributes.defaultRealm + Default realm + + + attributes.defaultRealm.help + Choose a default realm to secure any attribute value within API. This will affect only new attribute values. + + + attributes.defaultRealm.placeholder + + -- No default realm -- + Default text when no realm is attached to an attribute + + attributeValue.realm.placeholder + + -- No realm -- + Default text when no realm is attached to an attribute-value diff --git a/translations/core/messages.fr.xlf b/translations/core/messages.fr.xlf index c71a735b..0210b7a4 100644 --- a/translations/core/messages.fr.xlf +++ b/translations/core/messages.fr.xlf @@ -157,6 +157,24 @@ password_should_be_at_least_{{length}}_characters_long Le mot de passe doit posséder au minimum {{length}} caractères + + attributes.defaultRealm + Domaine sécurisé par défaut + + + attributes.defaultRealm.help + Choisissez un domaine sécurisé pour que tous les attributs créés soient protégés dans l'API. Cela n'affectera que les nouveaux attributs. + + + attributes.defaultRealm.placeholder + + -- Aucun domaine sécurisé par défaut -- + Default text when no realm is attached to an attribute + + attributeValue.realm.placeholder + + -- Aucun domaine sécurisé -- + Default text when no realm is attached to an attribute-value diff --git a/translations/core/messages.xlf b/translations/core/messages.xlf index 586b3624..ecf881ff 100644 --- a/translations/core/messages.xlf +++ b/translations/core/messages.xlf @@ -166,6 +166,24 @@ + + attributes.defaultRealm + + + + attributes.defaultRealm.help + + + + attributes.defaultRealm.placeholder + Default text when no realm is attached to an attribute + + + + attributeValue.realm.placeholder + Default text when no realm is attached to an attribute-value + + diff --git a/translations/security.ar.xlf b/translations/security.ar.xlf new file mode 100644 index 00000000..972f39e0 --- /dev/null +++ b/translations/security.ar.xlf @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/translations/security.de.xlf b/translations/security.de.xlf new file mode 100644 index 00000000..5d46fc4b --- /dev/null +++ b/translations/security.de.xlf @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/translations/security.en.xlf b/translations/security.en.xlf index 76189574..92492801 100644 --- a/translations/security.en.xlf +++ b/translations/security.en.xlf @@ -1,23 +1,23 @@ - - - + + + Your user account is not enabled. Contact an administrator. Your user account is not enabled. Contact an administrator. - + Your credentials have expired. Please request a new password. Your credentials have expired. Please request a new password. - + Your account has expired. Contact an administrator. Your account has expired. Contact an administrator. - + Your user account is locked. Contact an administrator. Your user account is locked. Contact an administrator. - - + + diff --git a/translations/security.es.xlf b/translations/security.es.xlf new file mode 100644 index 00000000..73274f66 --- /dev/null +++ b/translations/security.es.xlf @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/translations/security.fr.xlf b/translations/security.fr.xlf index 1b6f7a74..bc8f9ad4 100644 --- a/translations/security.fr.xlf +++ b/translations/security.fr.xlf @@ -1,23 +1,23 @@ - - - + + + Your user account is not enabled. Contact an administrator. Votre compte utilisateur n'est pas activé. Contactez un administrateur. - + Your credentials have expired. Please request a new password. - Vos informations d'identification ont expiré. Veuillez demander un nouveau mot de passe. + Vos informations de connexion ont expiré. Veuillez demander un nouveau mot de passe. - + Your account has expired. Contact an administrator. Votre compte a expiré. Contactez un administrateur. - + Your user account is locked. Contact an administrator. Votre compte utilisateur est verrouillé. Contactez un administrateur. - - + + diff --git a/translations/security.id.xlf b/translations/security.id.xlf new file mode 100644 index 00000000..10b490eb --- /dev/null +++ b/translations/security.id.xlf @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/translations/security.it.xlf b/translations/security.it.xlf new file mode 100644 index 00000000..1c087246 --- /dev/null +++ b/translations/security.it.xlf @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/translations/security.ru.xlf b/translations/security.ru.xlf new file mode 100644 index 00000000..bdafc723 --- /dev/null +++ b/translations/security.ru.xlf @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/translations/security.sr.xlf b/translations/security.sr.xlf new file mode 100644 index 00000000..a24d74fa --- /dev/null +++ b/translations/security.sr.xlf @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/translations/security.tr.xlf b/translations/security.tr.xlf new file mode 100644 index 00000000..fe017043 --- /dev/null +++ b/translations/security.tr.xlf @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/translations/security.uk.xlf b/translations/security.uk.xlf new file mode 100644 index 00000000..f37323e8 --- /dev/null +++ b/translations/security.uk.xlf @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/translations/security.zh.xlf b/translations/security.zh.xlf new file mode 100644 index 00000000..6b49359a --- /dev/null +++ b/translations/security.zh.xlf @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/translations/validators.ar.xlf b/translations/validators.ar.xlf index 7fcf2644..f4fb0597 100644 --- a/translations/validators.ar.xlf +++ b/translations/validators.ar.xlf @@ -4,6 +4,7 @@ + diff --git a/translations/validators.en.xlf b/translations/validators.en.xlf index 62c27ea0..1abb71e8 100644 --- a/translations/validators.en.xlf +++ b/translations/validators.en.xlf @@ -10,6 +10,10 @@ tagName.%name%.alreadyExists Tag %name% already exists. + + field_with_same_name_already_exists_but_with_different_doctrine_type + This field already exists with the same name in another node type, but with a different Doctrine data type. + diff --git a/translations/validators.es.xlf b/translations/validators.es.xlf index b98ab19c..bf1185dd 100644 --- a/translations/validators.es.xlf +++ b/translations/validators.es.xlf @@ -4,6 +4,7 @@ + diff --git a/translations/validators.fr.xlf b/translations/validators.fr.xlf index 0daaf454..6de39b55 100644 --- a/translations/validators.fr.xlf +++ b/translations/validators.fr.xlf @@ -10,6 +10,10 @@ tagName.%name%.alreadyExists L'étiquette «%name%» existe déjà. + + field_with_same_name_already_exists_but_with_different_doctrine_type + Ce champ existe déjà avec le même nom dans un autre type de nœud, mais avec un type de donnée Doctrine différent. + diff --git a/translations/validators.id.xlf b/translations/validators.id.xlf index 8d22e146..707d35fe 100644 --- a/translations/validators.id.xlf +++ b/translations/validators.id.xlf @@ -4,6 +4,7 @@ + diff --git a/translations/validators.it.xlf b/translations/validators.it.xlf index aa25ca67..0e106d9c 100644 --- a/translations/validators.it.xlf +++ b/translations/validators.it.xlf @@ -4,6 +4,7 @@ + diff --git a/translations/validators.ru.xlf b/translations/validators.ru.xlf index 2871bce1..12dceea9 100644 --- a/translations/validators.ru.xlf +++ b/translations/validators.ru.xlf @@ -4,6 +4,7 @@ + diff --git a/translations/validators.sr.xlf b/translations/validators.sr.xlf index d39ab964..11661084 100644 --- a/translations/validators.sr.xlf +++ b/translations/validators.sr.xlf @@ -4,6 +4,7 @@ + diff --git a/translations/validators.tr.xlf b/translations/validators.tr.xlf index 7d3ddf7b..1241fabb 100644 --- a/translations/validators.tr.xlf +++ b/translations/validators.tr.xlf @@ -4,6 +4,7 @@ + diff --git a/translations/validators.xlf b/translations/validators.xlf index 82fbfc32..413ebfd4 100644 --- a/translations/validators.xlf +++ b/translations/validators.xlf @@ -10,6 +10,10 @@ tagName.%name%.alreadyExists + + field_with_same_name_already_exists_but_with_different_doctrine_type + +