Skip to content

Commit 0f7ed9c

Browse files
committed
Import sermon categories
1 parent a055204 commit 0f7ed9c

3 files changed

Lines changed: 628 additions & 0 deletions

File tree

Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
<?php
2+
3+
namespace Psmb\PsmbImport\Command;
4+
5+
use Neos\Flow\Annotations as Flow;
6+
use Neos\Flow\Cli\CommandController;
7+
use Neos\ContentRepository\Domain\Service\ContextFactoryInterface;
8+
use Neos\ContentRepository\Domain\Service\NodeTypeManager;
9+
use Neos\ContentRepository\Domain\Model\NodeInterface;
10+
use Neos\ContentRepository\Domain\Model\NodeTemplate;
11+
use Neos\ContentRepository\Domain\Service\NodeServiceInterface;
12+
use Neos\Eel\FlowQuery\FlowQuery;
13+
use Neos\Flow\Persistence\PersistenceManagerInterface;
14+
use Psr\Log\LoggerInterface;
15+
16+
/**
17+
* CLI commands related to Sermon import and management.
18+
* @Flow\Scope("singleton")
19+
*/
20+
class SermonCommandController extends CommandController
21+
{
22+
/**
23+
* @Flow\Inject
24+
* @var ContextFactoryInterface
25+
*/
26+
protected ContextFactoryInterface $contextFactory;
27+
28+
/**
29+
* @Flow\Inject
30+
* @var NodeServiceInterface
31+
*/
32+
protected NodeServiceInterface $nodeService;
33+
34+
/**
35+
* @Flow\Inject
36+
* @var NodeTypeManager
37+
*/
38+
protected NodeTypeManager $nodeTypeManager;
39+
40+
/**
41+
* @Flow\Inject
42+
* @var PersistenceManagerInterface
43+
*/
44+
protected PersistenceManagerInterface $persistenceManager;
45+
46+
/**
47+
* @Flow\Inject
48+
* @var LoggerInterface
49+
*/
50+
protected LoggerInterface $logger;
51+
52+
/**
53+
* Assign categories to sermons based on JSON mapping files.
54+
*
55+
* Reads sermon data (hash + categories) and a hash-to-uriPathSegment mapping,
56+
* finds the corresponding sermon and category nodes (creating categories if needed),
57+
* and assigns the categories to the sermon's 'categories' property.
58+
*
59+
* @param bool $dryRun If set, no changes will be persisted
60+
*/
61+
public function assignCategoriesCommand(
62+
bool $dryRun = false
63+
): void {
64+
$workspace = 'live';
65+
$siteNodePath = '/sites/site';
66+
$sermonDataPath = '/data/www-provisioned/sermons_out.json';
67+
$hashToIdPath = '/data/www-provisioned/hashToSermonId.json';
68+
$sermonParentNodePath = 's';
69+
$categoryParentNodePath = 'tags/themes';
70+
$sermonNodeType = 'Sfi.Site:Sermon';
71+
$categoryNodeType = 'Sfi.Site:Tag';
72+
$categoryPropertyName = 'categories';
73+
74+
$context = $this->contextFactory->create(['workspaceName' => $workspace]);
75+
$siteNode = $context->getNode($siteNodePath);
76+
77+
if ($siteNode === null) {
78+
$this->outputLine('<error>Site node not found at path "%s"</error>', [$siteNodePath]);
79+
$this->quit(1);
80+
}
81+
82+
$sermonStorageNode = $siteNode->getNode($sermonParentNodePath);
83+
if ($sermonStorageNode === null) {
84+
$this->outputLine('<error>Sermon storage node not found at path "%s" relative to site node "%s"</error>', [$sermonParentNodePath, $siteNodePath]);
85+
$this->quit(1);
86+
}
87+
88+
$categoryStorageNode = $siteNode->getNode($categoryParentNodePath);
89+
if ($categoryStorageNode === null) {
90+
$this->outputLine('<comment>Category storage node not found at path "%s" relative to site node "%s". Attempting to create.</comment>', [$categoryParentNodePath, $siteNodePath]);
91+
// Logic to create category storage node if needed (e.g., using NodeTemplate)
92+
// For now, we'll error out if it doesn't exist, assuming it should be pre-created.
93+
// If creation is desired, implement similar logic as in the original SermonImporter::process()
94+
$this->outputLine('<error>Category storage node creation not implemented. Please ensure node exists at "%s".</error>', [$siteNodePath . '/' . $categoryParentNodePath]);
95+
$this->quit(1);
96+
// Placeholder: $categoryStorageNode = $this->createCategoryStorageNode($siteNode, $categoryParentNodePath);
97+
}
98+
99+
// --- Read JSON data ---
100+
if (!file_exists($sermonDataPath)) {
101+
$this->outputLine('<error>Sermon data file not found at "%s"</error>', [$sermonDataPath]);
102+
$this->quit(1);
103+
}
104+
if (!file_exists($hashToIdPath)) {
105+
$this->outputLine('<error>Hash to ID mapping file not found at "%s"</error>', [$hashToIdPath]);
106+
$this->quit(1);
107+
}
108+
109+
$sermonsData = json_decode(file_get_contents($sermonDataPath), true);
110+
if (json_last_error() !== JSON_ERROR_NONE) {
111+
$this->outputLine('<error>Error decoding sermon data JSON "%s": %s</error>', [$sermonDataPath, json_last_error_msg()]);
112+
$this->quit(1);
113+
}
114+
115+
$hashToIdMap = json_decode(file_get_contents($hashToIdPath), true);
116+
if (json_last_error() !== JSON_ERROR_NONE) {
117+
$this->outputLine('<error>Error decoding hash to ID map JSON "%s": %s</error>', [$hashToIdPath, json_last_error_msg()]);
118+
$this->quit(1);
119+
}
120+
121+
$processedCount = 0;
122+
$skippedCount = 0;
123+
$errorCount = 0;
124+
$nodesToPublish = [];
125+
126+
$this->outputLine('Starting category assignment process...');
127+
if ($dryRun) {
128+
$this->outputLine('<comment>Dry run enabled. No changes will be persisted.</comment>');
129+
}
130+
131+
// --- Process Sermons ---
132+
foreach ($sermonsData as $sermonEntry) {
133+
$hash = $sermonEntry['id'] ?? null;
134+
$categoryNames = $sermonEntry['categories'] ?? [];
135+
136+
if (!$hash || empty($categoryNames)) {
137+
$this->logger->warning(sprintf('Skipping entry due to missing hash or categories: %s', json_encode($sermonEntry, JSON_UNESCAPED_UNICODE)));
138+
$skippedCount++;
139+
continue;
140+
}
141+
142+
$uriPathSegment = $hashToIdMap[$hash] ?? null;
143+
if (!$uriPathSegment) {
144+
$this->logger->warning(sprintf('Skipping sermon hash %s: uriPathSegment not found in map.', $hash));
145+
$skippedCount++;
146+
continue;
147+
}
148+
149+
/** @var NodeInterface $sermonNode */
150+
$sermonNode = $sermonStorageNode->getNode($uriPathSegment);
151+
152+
if ($sermonNode === null) {
153+
$this->logger->warning(sprintf('Skipping sermon hash %s: Node with pathSegment "%s" not found under %s.', $hash, $uriPathSegment, $sermonStorageNode->getPath()));
154+
$skippedCount++;
155+
continue;
156+
}
157+
158+
if ($sermonNode->getNodeType()->getName() !== $sermonNodeType) {
159+
$this->logger->warning(sprintf('Skipping node %s: Expected type %s, but got %s.', $sermonNode->getPath(), $sermonNodeType, $sermonNode->getNodeType()->getName()));
160+
$skippedCount++;
161+
continue;
162+
}
163+
164+
try {
165+
$categoryNodes = [];
166+
foreach ($categoryNames as $categoryName) {
167+
$categoryNode = $this->getOrCreateCategoryByName($categoryName, $categoryStorageNode, $categoryNodeType, $dryRun);
168+
if ($categoryNode) {
169+
$categoryNodes[] = $categoryNode;
170+
// Track nodes that might need publishing (newly created categories)
171+
if (!isset($nodesToPublish[$categoryNode->getPath()])) {
172+
$nodesToPublish[$categoryNode->getPath()] = $categoryNode;
173+
}
174+
}
175+
}
176+
177+
if (!empty($categoryNodes)) {
178+
$this->outputLine('Updating categories for sermon: %s', [$sermonNode->getPath()]);
179+
if (!$dryRun) {
180+
$sermonNode->setProperty($categoryPropertyName, $categoryNodes);
181+
if (!isset($nodesToPublish[$sermonNode->getPath()])) {
182+
$nodesToPublish[$sermonNode->getPath()] = $sermonNode;
183+
}
184+
}
185+
$this->logger->info(sprintf('Assigned %d categories to sermon %s', count($categoryNodes), $sermonNode->getPath()));
186+
$processedCount++;
187+
} else {
188+
$this->logger->notice(sprintf('No valid category nodes found/created for sermon %s', $sermonNode->getPath()));
189+
$skippedCount++;
190+
}
191+
} catch (\Exception $e) {
192+
$this->logger->error(sprintf('Error processing sermon %s (%s): %s', $uriPathSegment, $hash, $e->getMessage()), ['exception' => $e]);
193+
$this->outputLine('<error>Error processing sermon %s (%s): %s</error>', [$uriPathSegment, $hash, $e->getMessage()]);
194+
$errorCount++;
195+
}
196+
}
197+
198+
// --- Publish Changes ---
199+
if (!$dryRun && count($nodesToPublish) > 0) {
200+
$this->outputLine('Publishing %d changed nodes...', [count($nodesToPublish)]);
201+
// In Flow > 7, publishNodes might be preferred if available and suitable.
202+
// For broader compatibility, persistAll() is used here. Consider implications.
203+
try {
204+
$this->persistenceManager->persistAll();
205+
$this->outputLine('<success>Changes persisted successfully.</success>');
206+
} catch (\Exception $e) {
207+
$this->logger->error('Error persisting changes: ' . $e->getMessage(), ['exception' => $e]);
208+
$this->outputLine('<error>Error persisting changes: %s</error>', [$e->getMessage()]);
209+
$errorCount++; // Count persistence errors
210+
}
211+
} elseif ($dryRun) {
212+
$this->outputLine('Dry run finished. %d nodes would have been persisted.', [count($nodesToPublish)]);
213+
} else {
214+
$this->outputLine('No changes to persist.');
215+
}
216+
217+
218+
$this->outputLine('Category assignment finished.');
219+
$this->outputLine('Processed: %d, Skipped: %d, Errors: %d', [$processedCount, $skippedCount, $errorCount]);
220+
$this->quit($errorCount > 0 ? 1 : 0);
221+
}
222+
223+
/**
224+
* Finds or creates a category tag node by its name under the given parent node.
225+
*
226+
* @param string $name The name (title) of the category
227+
* @param NodeInterface $parentNode The parent node for categories
228+
* @param string $nodeTypeName The node type for the category tag
229+
* @param bool $dryRun If true, node creation will be skipped
230+
* @return NodeInterface|null The found or created category node, or null on error/skip
231+
* @throws \Exception
232+
*/
233+
protected function getOrCreateCategoryByName(string $name, NodeInterface $parentNode, string $nodeTypeName, bool $dryRun): ?NodeInterface
234+
{
235+
$trimmedName = trim($name);
236+
if (empty($trimmedName)) {
237+
$this->logger->warning('Skipping category creation/retrieval: name is empty.');
238+
return null;
239+
}
240+
241+
$context = $parentNode->getContext(); // Use context from parent
242+
243+
// Sanitize name for query. Basic quote escaping. Consider more robust slugification/sanitization if needed.
244+
$sanitizedName = str_replace('"', '\"', $trimmedName);
245+
246+
$q = new FlowQuery([$parentNode]);
247+
/** @var NodeInterface $node */
248+
$node = $q->find(sprintf('[instanceof %s]', $nodeTypeName))
249+
->filter(sprintf('[title = "%s"]', $sanitizedName))
250+
->get(0);
251+
252+
if ($node === null) {
253+
$this->logger->info(sprintf('Category "%s" not found, creating new node.', $trimmedName));
254+
if ($dryRun) {
255+
$this->outputLine('Dry Run: Would create category "%s"', [$trimmedName]);
256+
return null; // Cannot return a non-existent node in dry run
257+
}
258+
259+
$nodeTemplate = new NodeTemplate();
260+
$nodeTemplate->setNodeType($this->nodeTypeManager->getNodeType($nodeTypeName));
261+
$nodeTemplate->setProperty('title', $trimmedName);
262+
263+
try {
264+
$node = $parentNode->createNodeFromTemplate($nodeTemplate);
265+
$this->logger->info(sprintf('Created category node: %s', $node->getPath()));
266+
$this->outputLine('Created category: %s', [$node->getPath()]);
267+
// No need to persist here, handled at the end
268+
return $node;
269+
} catch (\Exception $e) {
270+
$this->logger->error(sprintf('Failed to create category node "%s": %s', $trimmedName, $e->getMessage()), ['exception' => $e]);
271+
$this->outputLine('<error>Failed to create category node "%s": %s</error>', [$trimmedName, $e->getMessage()]);
272+
// Re-throw or return null depending on desired error handling
273+
// Returning null to allow the main loop to continue
274+
return null;
275+
}
276+
} else {
277+
$this->logger->debug(sprintf('Found existing category "%s": %s', $trimmedName, $node->getPath()));
278+
return $node;
279+
}
280+
}
281+
}

0 commit comments

Comments
 (0)