vendor/sulu/sulu/src/Sulu/Component/Content/Document/Subscriber/StructureSubscriber.php line 342

Open in your IDE?
  1. <?php
  2. /*
  3. * This file is part of Sulu.
  4. *
  5. * (c) Sulu GmbH
  6. *
  7. * This source file is subject to the MIT license that is bundled
  8. * with this source code in the file LICENSE.
  9. */
  10. namespace Sulu\Component\Content\Document\Subscriber;
  11. use PHPCR\NodeInterface;
  12. use Sulu\Bundle\DocumentManagerBundle\Bridge\DocumentInspector;
  13. use Sulu\Component\Content\Compat\Structure\LegacyPropertyFactory;
  14. use Sulu\Component\Content\ContentTypeManagerInterface;
  15. use Sulu\Component\Content\Document\Behavior\LocalizedStructureBehavior;
  16. use Sulu\Component\Content\Document\Behavior\ShadowLocaleBehavior;
  17. use Sulu\Component\Content\Document\Behavior\StructureBehavior;
  18. use Sulu\Component\Content\Document\LocalizationState;
  19. use Sulu\Component\Content\Document\Structure\ManagedStructure;
  20. use Sulu\Component\Content\Document\Structure\Structure;
  21. use Sulu\Component\Content\Document\Structure\StructureInterface;
  22. use Sulu\Component\Content\Document\Subscriber\PHPCR\SuluNode;
  23. use Sulu\Component\Content\Exception\MandatoryPropertyException;
  24. use Sulu\Component\DocumentManager\Event\AbstractMappingEvent;
  25. use Sulu\Component\DocumentManager\Event\ConfigureOptionsEvent;
  26. use Sulu\Component\DocumentManager\Event\PersistEvent;
  27. use Sulu\Component\DocumentManager\Events;
  28. use Sulu\Component\DocumentManager\PropertyEncoder;
  29. use Sulu\Component\Webspace\Manager\WebspaceManagerInterface;
  30. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  31. class StructureSubscriber implements EventSubscriberInterface
  32. {
  33. public const STRUCTURE_TYPE_FIELD = 'template';
  34. /**
  35. * @param array $defaultTypes
  36. */
  37. public function __construct(
  38. private PropertyEncoder $encoder,
  39. private ContentTypeManagerInterface $contentTypeManager,
  40. private DocumentInspector $inspector,
  41. private LegacyPropertyFactory $legacyPropertyFactory,
  42. private WebspaceManagerInterface $webspaceManager,
  43. private $defaultTypes,
  44. ) {
  45. }
  46. public static function getSubscribedEvents()
  47. {
  48. return [
  49. Events::PERSIST => [
  50. // persist should happen before content is mapped
  51. ['saveStructureData', 0],
  52. // staged properties must be commited before title subscriber
  53. ['handlePersistStagedProperties', 50],
  54. // setting the structure should happen very early
  55. ['handlePersistStructureType', 100],
  56. ],
  57. Events::PUBLISH => 'saveStructureData',
  58. // hydrate should happen afterwards
  59. Events::HYDRATE => ['handleHydrate', 0],
  60. Events::CONFIGURE_OPTIONS => 'configureOptions',
  61. ];
  62. }
  63. public function configureOptions(ConfigureOptionsEvent $event)
  64. {
  65. $options = $event->getOptions();
  66. $options->setDefaults(
  67. [
  68. 'load_ghost_content' => true,
  69. 'clear_missing_content' => false,
  70. 'ignore_required' => false,
  71. 'structure_type' => null,
  72. ]
  73. );
  74. $options->setAllowedTypes('load_ghost_content', 'bool');
  75. $options->setAllowedTypes('clear_missing_content', 'bool');
  76. $options->setAllowedTypes('ignore_required', 'bool');
  77. }
  78. /**
  79. * Set the structure type early so that subsequent subscribers operate
  80. * upon the correct structure type.
  81. */
  82. public function handlePersistStructureType(PersistEvent $event)
  83. {
  84. $document = $event->getDocument();
  85. if (!$this->supportsBehavior($document)) {
  86. return;
  87. }
  88. $structureMetadata = $this->inspector->getStructureMetadata($document);
  89. $structure = $document->getStructure();
  90. if ($structure instanceof ManagedStructure) {
  91. $structure->setStructureMetadata($structureMetadata);
  92. }
  93. }
  94. /**
  95. * Commit the properties, which are only staged on the structure yet.
  96. */
  97. public function handlePersistStagedProperties(PersistEvent $event)
  98. {
  99. $document = $event->getDocument();
  100. if (!$this->supportsBehavior($document)) {
  101. return;
  102. }
  103. $document->getStructure()->commitStagedData($event->getOption('clear_missing_content'));
  104. }
  105. public function handleHydrate(AbstractMappingEvent $event)
  106. {
  107. $document = $event->getDocument();
  108. if (!$this->supportsBehavior($document)) {
  109. return;
  110. }
  111. $rehydrate = $event->getOption('rehydrate');
  112. $structureType = $this->getStructureType($event, $document, $rehydrate);
  113. $document->setStructureType($structureType);
  114. if (false === $event->getOption('load_ghost_content', false)) {
  115. if (LocalizationState::GHOST === $this->inspector->getLocalizationState($document)) {
  116. $structureType = null;
  117. }
  118. }
  119. $structure = $this->getStructure($document, $structureType, $rehydrate);
  120. // Set the property container
  121. $event->getAccessor()->set(
  122. 'structure',
  123. $structure
  124. );
  125. }
  126. public function saveStructureData(AbstractMappingEvent $event)
  127. {
  128. // Set the structure type
  129. $document = $event->getDocument();
  130. if (!$this->supportsBehavior($document)) {
  131. return;
  132. }
  133. if (!$document->getStructureType()) {
  134. return;
  135. }
  136. if (!$event->getLocale()) {
  137. return;
  138. }
  139. $node = $event->getNode();
  140. $locale = $event->getLocale();
  141. $options = $event->getOptions();
  142. $this->mapContentToNode($document, $node, $locale, $options['ignore_required']);
  143. $node->setProperty(
  144. $this->getStructureTypePropertyName($document, $locale),
  145. $document->getStructureType()
  146. );
  147. }
  148. /**
  149. * @param bool $rehydrate
  150. *
  151. * @return string
  152. */
  153. private function getStructureType(AbstractMappingEvent $event, StructureBehavior $document, $rehydrate)
  154. {
  155. $structureType = $event->getOption('structure_type');
  156. if ($structureType) {
  157. return $structureType;
  158. }
  159. $node = $event->getNode();
  160. $locale = $event->getLocale();
  161. if ($document instanceof ShadowLocaleBehavior && $document->isShadowLocaleEnabled()) {
  162. $locale = $document->getOriginalLocale();
  163. }
  164. $propertyName = $this->getStructureTypePropertyName($document, $locale);
  165. $structureType = $node->getPropertyValueWithDefault($propertyName, null);
  166. if (!$structureType && $rehydrate) {
  167. return $this->getDefaultStructureType($document);
  168. }
  169. return $structureType;
  170. }
  171. /**
  172. * Return the default structure for the given StructureBehavior implementing document.
  173. *
  174. * @return string
  175. */
  176. private function getDefaultStructureType(StructureBehavior $document)
  177. {
  178. $alias = $this->inspector->getMetadata($document)->getAlias();
  179. $webspace = $this->webspaceManager->findWebspaceByKey($this->inspector->getWebspace($document));
  180. if (!$webspace) {
  181. return $this->getDefaultStructureTypeFromConfig($alias);
  182. }
  183. return $webspace->getDefaultTemplate($alias);
  184. }
  185. /**
  186. * Returns configured "default_type".
  187. *
  188. * @param string $alias
  189. *
  190. * @return string
  191. */
  192. private function getDefaultStructureTypeFromConfig($alias)
  193. {
  194. if (!\array_key_exists($alias, $this->defaultTypes)) {
  195. return;
  196. }
  197. return $this->defaultTypes[$alias];
  198. }
  199. private function supportsBehavior($document)
  200. {
  201. return $document instanceof StructureBehavior;
  202. }
  203. private function getStructureTypePropertyName($document, $locale)
  204. {
  205. if ($document instanceof LocalizedStructureBehavior) {
  206. return $this->encoder->localizedSystemName(self::STRUCTURE_TYPE_FIELD, $locale);
  207. }
  208. // TODO: This is the wrong namespace, it should be the system namespcae, but we do this for initial BC
  209. return $this->encoder->contentName(self::STRUCTURE_TYPE_FIELD);
  210. }
  211. /**
  212. * @return ManagedStructure
  213. */
  214. private function createStructure($document)
  215. {
  216. return new ManagedStructure(
  217. $this->contentTypeManager,
  218. $this->legacyPropertyFactory,
  219. $this->inspector,
  220. $document
  221. );
  222. }
  223. /**
  224. * Map to the content properties to the node using the content types.
  225. *
  226. * @param string $locale
  227. * @param bool $ignoreRequired
  228. *
  229. * @throws MandatoryPropertyException
  230. * @throws \RuntimeException
  231. */
  232. private function mapContentToNode($document, NodeInterface $node, $locale, $ignoreRequired)
  233. {
  234. $structure = $document->getStructure();
  235. $webspaceName = $this->inspector->getWebspace($document);
  236. $metadata = $this->inspector->getStructureMetadata($document);
  237. if (!$metadata) {
  238. throw new \RuntimeException(
  239. \sprintf(
  240. 'Metadata for Structure Type "%s" was not found, does the file "%s.xml" exists?',
  241. $document->getStructureType(),
  242. $document->getStructureType()
  243. )
  244. );
  245. }
  246. foreach ($metadata->getProperties() as $propertyName => $structureProperty) {
  247. if (TitleSubscriber::PROPERTY_NAME === $propertyName) {
  248. continue;
  249. }
  250. $realProperty = $structure->getProperty($propertyName);
  251. $value = $realProperty->getValue();
  252. if (false === $ignoreRequired && $structureProperty->isRequired() && null === $value) {
  253. throw new MandatoryPropertyException(
  254. \sprintf(
  255. 'Property "%s" in structure "%s" is required but no value was given. Loaded from "%s"',
  256. $propertyName,
  257. $metadata->getName(),
  258. $metadata->getResource()
  259. )
  260. );
  261. }
  262. $contentTypeName = $structureProperty->getType();
  263. $contentType = $this->contentTypeManager->get($contentTypeName);
  264. // TODO: Only write if the property has been modified.
  265. $legacyProperty = $this->legacyPropertyFactory->createTranslatedProperty($structureProperty, $locale);
  266. $legacyProperty->setValue($value);
  267. $contentType->write(
  268. new SuluNode($node),
  269. $legacyProperty,
  270. null,
  271. $webspaceName,
  272. $locale,
  273. null
  274. );
  275. }
  276. }
  277. /**
  278. * Return the a structure for the document.
  279. *
  280. * - If the Structure already exists on the document, use that.
  281. * - If the Structure type is given, then create a ManagedStructure - this
  282. * means that the structure is already persisted on the node and it has data.
  283. * - If none of the above applies then create a new, empty, Structure.
  284. *
  285. * @param object $document
  286. * @param string $structureType
  287. * @param bool $rehydrate
  288. *
  289. * @return StructureInterface
  290. */
  291. private function getStructure($document, $structureType, $rehydrate)
  292. {
  293. if ($structureType) {
  294. return $this->createStructure($document);
  295. }
  296. if (!$rehydrate && $document->getStructure()) {
  297. return $document->getStructure();
  298. }
  299. return new Structure();
  300. }
  301. }