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

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