WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions PhpcrMigration/Application/Exception/InvalidDocumentException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

/*
* This file is part of Sulu.
*
* (c) Sulu GmbH
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/

namespace Sulu\Bundle\PhpcrMigrationBundle\PhpcrMigration\Application\Exception;

class InvalidDocumentException extends \RuntimeException
{
public function __construct(string $reason)
{
parent::__construct(\sprintf('The document is invalid: %s', $reason));
}
}
10 changes: 7 additions & 3 deletions PhpcrMigration/Application/Parser/ArticleNodeParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,16 @@
use Sulu\Bundle\PhpcrMigrationBundle\PhpcrMigration\Application\Exception\LegacyRouteTableNotFoundException;
use Sulu\Bundle\PhpcrMigrationBundle\PhpcrMigration\Application\Persister\AbstractPersister;
use Sulu\Bundle\PhpcrMigrationBundle\PhpcrMigration\Application\Repository\EntityRepositoryInterface;
use Sulu\Bundle\PhpcrMigrationBundle\PhpcrMigration\Application\Service\LocaleDiscoveryService;

class ArticleNodeParser implements NodeParserInterface
{
public const LEGACY_ROUTE_TABLE = 'ro_routes_old';

public function __construct(private readonly EntityRepositoryInterface $repository)
{
public function __construct(
private readonly EntityRepositoryInterface $repository,
private readonly LocaleDiscoveryService $localeDiscoveryService,
) {
}

public function parse(NodeInterface $node, string $documentType): array
Expand Down Expand Up @@ -71,6 +74,7 @@ private function parseLocalizedRoutes(NodeInterface $node, array $localizations)
]
);

$discoveredLocales = $this->localeDiscoveryService->discoverLocales($node);
foreach ($routes as $route) {
if (!\is_array($route)) {
continue;
Expand All @@ -79,7 +83,7 @@ private function parseLocalizedRoutes(NodeInterface $node, array $localizations)
$locale = $route['locale'] ?? null;
$url = $route['path'] ?? null;

if ($locale && $url) {
if ($locale && $url && isset($discoveredLocales[$locale])) {
match ((bool) ($route['history'] ?? false)) {
true => $localizations[$locale][AbstractPersister::HISTORY_URLS][] = $url,
false => $localizations[$locale][AbstractPersister::URL] = $url,
Expand Down
11 changes: 11 additions & 0 deletions PhpcrMigration/Application/Parser/PageNodeParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,15 @@

use PHPCR\NodeInterface;
use Sulu\Bundle\PhpcrMigrationBundle\PhpcrMigration\Application\Persister\AbstractPersister;
use Sulu\Bundle\PhpcrMigrationBundle\PhpcrMigration\Application\Service\LocaleDiscoveryService;

class PageNodeParser implements NodeParserInterface
{
public function __construct(
private readonly LocaleDiscoveryService $localeDiscoveryService,
) {
}

public function parse(NodeInterface $node, string $documentType): array
{
if (!$this->supports($node, $documentType)) {
Expand Down Expand Up @@ -60,12 +66,17 @@ private function supports(NodeInterface $node, string $documentType): bool
*/
private function parseLocalizedRoutes(NodeInterface $node, array $localizations): array
{
$discoveredLocales = $this->localeDiscoveryService->discoverLocales($node);
foreach ($node->getReferences('sulu:content') as $reference) {
$route = $reference->getParent();
$routePath = $route->getPath();
$locale = \explode('/', $routePath)[4];
$slug = '/' . (\explode('/', $routePath, 6)[5] ?? '');

if (!isset($discoveredLocales[$locale])) {
continue;
}

$historyReferences = $route->getReferences();
$historyUrls = [];
foreach ($historyReferences as $historyReference) {
Expand Down
32 changes: 22 additions & 10 deletions PhpcrMigration/Application/Parser/PropertyNodeParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@
use Jackalope\Property;
use PHPCR\NodeInterface;
use PHPCR\PropertyInterface;
use Sulu\Bundle\PhpcrMigrationBundle\PhpcrMigration\Application\Service\LocaleDiscoveryService;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;

class PropertyNodeParser implements NodeParserInterface
{
public function __construct(
private readonly PropertyAccessorInterface $propertyAccessor,
private readonly LocaleDiscoveryService $localeDiscoveryService,
) {
}

Expand All @@ -43,8 +45,9 @@ public function parse(NodeInterface $node, string $documentType): array
'sulu' => [],
'jcr' => [],
];
$discoveredLocales = $this->localeDiscoveryService->discoverLocales($node);
foreach ($node->getProperties() as $property) {
$document = $this->parseProperty($property, $document);
$document = $this->parseProperty($property, $document, $discoveredLocales);
}

/** @var array<string, array<string, mixed>> $localizations */
Expand Down Expand Up @@ -87,14 +90,15 @@ public function parse(NodeInterface $node, string $documentType): array

/**
* @param mixed[] $document
* @param string[] $knownLocales
*
* @return mixed[]
*/
private function parseProperty(PropertyInterface $property, array $document): array
private function parseProperty(PropertyInterface $property, array $document, array $knownLocales): array
{
$name = $property->getName();
$value = $this->resolvePropertyValue($property);
$propertyPath = $this->getLocalizedPath($name);
$propertyPath = $this->getLocalizedPath($name, $knownLocales);
$propertyPath = $this->getPropertyPath($propertyPath, $name);

$this->propertyAccessor->setValue(
Expand All @@ -121,17 +125,25 @@ private function resolvePropertyValue(PropertyInterface $property): mixed
return $value;
}

private function getLocalizedPath(string &$name): string
/**
* @param string[] $locales
*/
private function getLocalizedPath(string &$name, array $locales = []): string
{
$propertyPath = '';
if (\str_starts_with($name, 'i18n:')) {
$localizationOffset = 5;
$firstDashPosition = \strpos($name, '-', $localizationOffset);
$locale = \substr($name, 5, $firstDashPosition - $localizationOffset);
$propertyPath .= '[localizations][' . $locale . ']';
$name = \substr($name, $firstDashPosition + 1);
$afterPrefix = \substr($name, 5);

foreach ($locales as $locale) {
if (\str_starts_with($afterPrefix, $locale . '-')) {
$propertyPath = '[localizations][' . $locale . ']';
$name = \substr($afterPrefix, \strlen($locale) + 1);

return $propertyPath;
}
}
} elseif ($this->isUnLocalizedProperty($name)) {
$propertyPath .= '[localizations][null]';
$propertyPath = '[localizations][null]';
}

return $propertyPath;
Expand Down
83 changes: 45 additions & 38 deletions PhpcrMigration/Application/Persister/AbstractPersister.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

namespace Sulu\Bundle\PhpcrMigrationBundle\PhpcrMigration\Application\Persister;

use Sulu\Bundle\PhpcrMigrationBundle\PhpcrMigration\Application\Exception\InvalidDocumentException;
use Sulu\Bundle\PhpcrMigrationBundle\PhpcrMigration\Application\Exception\UnsupportedDocumentTypeException;
use Sulu\Bundle\PhpcrMigrationBundle\PhpcrMigration\Application\Repository\EntityRepositoryInterface;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
Expand Down Expand Up @@ -385,49 +386,55 @@ protected function createOrUpdateDimensionContent(array $document, bool $isLive)
$localizedData['_seoData'] = $this->buildSeoData($localizedData);
$localizedData['_excerptData'] = $this->buildExcerptData($localizedData);

$data = $this->mapDataViaMapping($localizedData, $this->getDimensionContentMapping());
$created = $document['sulu']['created'] ?? null;
$data['created'] = $created instanceof \DateTimeInterface ? $created->format('Y-m-d H:i:s') : null;
$changed = $document['sulu']['changed'] ?? null;
$data['changed'] = $changed instanceof \DateTimeInterface ? $changed->format('Y-m-d H:i:s') : null;

$data = \array_merge($this->getDefaultData(), $data);
$data = $this->mapDimensionContentData($document, $locale, $data, $isLive);

$data = $this->validateData($data);

// remove known keys that do not belong to the templateData
$localizedData = $this->removeNonTemplateData($localizedData);

/** @var mixed[] $templateData */
$templateData = $data['templateData'] ?? [];
$data['templateData'] = \array_merge($localizedData, $templateData);
$data['templateData'] = $this->addBlockIds($data['templateData']);

// SULU 3.0 MIGRATION FIX: Ensure ALL data is UTF-8 encoded (not just templateData)
// This fixes title, seoTitle, excerptTitle, excerptDescription, and all other string fields
$fixedData = $this->fixUtf8Encoding($data);
\assert(\is_array($fixedData));
$data = $fixedData;
try {
$data = $this->mapDataViaMapping($localizedData, $this->getDimensionContentMapping());
$created = $document['sulu']['created'] ?? null;
$data['created'] = $created instanceof \DateTimeInterface ? $created->format('Y-m-d H:i:s') : null;
$changed = $document['sulu']['changed'] ?? null;
$data['changed'] = $changed instanceof \DateTimeInterface ? $changed->format('Y-m-d H:i:s') : null;

$data = \array_merge($this->getDefaultData(), $data);
$data = $this->mapDimensionContentData($document, $locale, $data, $isLive);

$data = $this->validateData($data);

// remove known keys that do not belong to the templateData
$localizedData = $this->removeNonTemplateData($localizedData);

/** @var mixed[] $templateData */
$templateData = $data['templateData'] ?? [];
$data['templateData'] = \array_merge($localizedData, $templateData);
$data['templateData'] = $this->addBlockIds($data['templateData']);

// SULU 3.0 MIGRATION FIX: Ensure ALL data is UTF-8 encoded (not just templateData)
// This fixes title, seoTitle, excerptTitle, excerptDescription, and all other string fields
$fixedData = $this->fixUtf8Encoding($data);
\assert(\is_array($fixedData));
$data = $fixedData;
} catch (InvalidDocumentException $e) {
echo \sprintf(
"Skipping dimension content for document with UUID '%s' and locale '%s': %s\n",
$document['jcr']['uuid'],
$locale ?? 'null',
$e->getMessage()
);
continue;
}

$data['version'] = 0;

$entityIdMappingName = $this->getDimensionContentEntityIdMappingName();

try {
$this->entityRepository->insertOrUpdate(
$data,
$this->getDimensionContentTableName(),
$this->getDimensionContentTableTypes(),
[
$entityIdMappingName => $data[$entityIdMappingName],
'locale' => $locale,
'stage' => $data['stage'],
],
);
} catch (\Exception $e) {
throw $e;
}
$this->entityRepository->insertOrUpdate(
$data,
$this->getDimensionContentTableName(),
$this->getDimensionContentTableTypes(),
[
$entityIdMappingName => $data[$entityIdMappingName],
'locale' => $locale,
'stage' => $data['stage'],
],
);

/**
* @var DimensionContent $dimensionContent
Expand Down
9 changes: 9 additions & 0 deletions PhpcrMigration/Application/Persister/PagePersister.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

namespace Sulu\Bundle\PhpcrMigrationBundle\PhpcrMigration\Application\Persister;

use Sulu\Bundle\PhpcrMigrationBundle\PhpcrMigration\Application\Exception\InvalidDocumentException;
use Sulu\Bundle\PhpcrMigrationBundle\PhpcrMigration\Application\Exception\InvalidPathException;
use Sulu\Bundle\PhpcrMigrationBundle\PhpcrMigration\Application\Repository\EntityRepositoryInterface;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
Expand Down Expand Up @@ -114,6 +115,14 @@ private function mapShadowLocaleData(array $document, ?string $locale, array $da
$data['shadowLocale'] = ($shadowOn && \is_string($shadowBase)) ? $shadowBase : null;
$data['shadowLocales'] = null;

if (isset($data['shadowLocale'])) {
$shadowTemplateKey = $document['localizations'][$locale]['template'] ?? null;
if (null === $shadowTemplateKey) {
throw new InvalidDocumentException('Template key of shadow locale is missing.');
}
$data['templateKey'] = $shadowTemplateKey;
}

return $data;
}

Expand Down
77 changes: 77 additions & 0 deletions PhpcrMigration/Application/Service/LocaleDiscoveryService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

declare(strict_types=1);

/*
* This file is part of Sulu.
*
* (c) Sulu GmbH
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/

namespace Sulu\Bundle\PhpcrMigrationBundle\PhpcrMigration\Application\Service;

use PHPCR\NodeInterface;

class LocaleDiscoveryService
{
/**
* @var array<string, string[]>
*/
private array $localeCache = [];

/**
* @return string[]
*/
public function discoverLocales(NodeInterface $node): array
{
$nodeIdentifier = $node->getIdentifier();
if (isset($this->localeCache[$nodeIdentifier])) {
return $this->localeCache[$nodeIdentifier];
}

$localesWithTitle = [];
$localesWithTemplate = [];
$discoveredLocales = [];
foreach ($node->getProperties('i18n:*') as $property) {
$name = $property->getName();

$afterPrefix = \substr($name, 5);

if (\str_ends_with($afterPrefix, '-changed')) {
$locale = \substr($afterPrefix, 0, -\strlen('-changed'));

if (!isset($discoveredLocales[$locale])) {
$localesWithTitle[$locale] = true;

if (isset($localesWithTemplate[$locale])) {
$discoveredLocales[$locale] = $locale;
}
}
}

if (\str_ends_with($afterPrefix, '-created')) {
$locale = \substr($afterPrefix, 0, -\strlen('-created'));

if (!isset($discoveredLocales[$locale])) {
$localesWithTemplate[$locale] = true;

if (isset($localesWithTitle[$locale])) {
$discoveredLocales[$locale] = $locale;
}
}
}
}

$this->localeCache[$nodeIdentifier] = $discoveredLocales;

return $discoveredLocales;
}

public function clearCache(): void
{
$this->localeCache = [];
}
}
7 changes: 6 additions & 1 deletion Resources/config/parser.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,27 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="sulu_phpcr_migration.locale_discovery_service"
class="Sulu\Bundle\PhpcrMigrationBundle\PhpcrMigration\Application\Service\LocaleDiscoveryService"/>

<service id="sulu_phpcr_migration.node_parser"
class="Sulu\Bundle\PhpcrMigrationBundle\PhpcrMigration\Application\Parser\PropertyNodeParser">
<argument type="service" id="property_accessor"/>
<argument type="service" id="sulu_phpcr_migration.locale_discovery_service"/>

<tag name="sulu_phpcr_migration.node_parser"/>
</service>

<service id="sulu_phpcr_migration.page_parser"
class="Sulu\Bundle\PhpcrMigrationBundle\PhpcrMigration\Application\Parser\PageNodeParser">

<argument type="service" id="sulu_phpcr_migration.locale_discovery_service"/>
<tag name="sulu_phpcr_migration.node_parser"/>
</service>

<service id="sulu_phpcr_migration.article_parser"
class="Sulu\Bundle\PhpcrMigrationBundle\PhpcrMigration\Application\Parser\ArticleNodeParser">
<argument type="service" id="sulu_phpcr_migration.entity_repository"/>
<argument type="service" id="sulu_phpcr_migration.locale_discovery_service"/>

<tag name="sulu_phpcr_migration.node_parser"/>
</service>
Expand Down