diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..199372a
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,34 @@
+name: Continuous Integration
+
+on: [push]
+
+jobs:
+ analyse:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v2
+
+ - name: Set up PHP environment
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: '8.1'
+
+ - name: Validate composer.json
+ run: composer validate
+
+ - name: Install dependencies
+ run: composer install
+
+ - name: Run tests
+ run: composer test
+
+ - name: Check coding standards
+ run: composer cs-check
+
+ - name: Check static analysis (PHPStan)
+ run: composer stan
+
+ - name: Check messdetector
+ run: composer md-check
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..2b57c4f
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,42 @@
+# IDE
+.idea/
+.project/
+nbproject/
+.buildpath/
+.settings/
+*.sublime-*
+src/Generated/*
+src/Orm/*
+
+# OS
+.DS_Store
+*.AppleDouble
+*.AppleDB
+*.AppleDesktop
+
+# grunt stuff
+.grunt
+.sass-cache
+/node_modules/
+
+# tooling
+vendor/
+composer.lock
+auth.json
+.phpunit.result.cache
+
+# built client resources
+src/*/Zed/*/Static/Public
+src/*/Zed/*/Static/Assets/sprite
+
+# Propel classes
+src/*/Zed/*/Persistence/Propel/Base/*
+src/*/Zed/*/Persistence/Propel/Map/*
+
+# tests
+tests/**/_generated/
+tests/_output/*
+!tests/_output/.gitkeep
+tests/app/*
+src/Orm
+src/Generated
\ No newline at end of file
diff --git a/README.md b/README.md
index c6b8ec3..334297f 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,13 @@
-# navigation-generator
\ No newline at end of file
+# Navigation Generator
+
+## Description:
+Adds new console command to spryker, which uses the imported category data to generate a navigation_node.csv based on the category-tree.
+
+## Usage:
+```
+vendor/bin/console data:generate:navigation-node
+```
+### Configuration:
+* `NavigationGeneratorConstants::OUTPUT_PATH` (Default: `APPLICATION_ROOT_DIR . '/data/import/common/common/navigation_node.csv'`)
+* `NavigationGeneratorConstants::FALLBACK_LOCALE` (Default: `de_DE`, is used when for a configured locale no category data is available)
+* `NavigationGeneratorConstants::NAVIGATION_KEY` (Default: `MAIN_NAVIGATION`)
diff --git a/codeception.yml b/codeception.yml
new file mode 100644
index 0000000..cb3e967
--- /dev/null
+++ b/codeception.yml
@@ -0,0 +1,40 @@
+namespace: ValanticSpryker
+
+suites:
+ unit:
+ path: .
+
+settings:
+ shuffle: true
+ lint: true
+
+bootstrap: _bootstrap.php
+
+paths:
+ tests: tests
+ output: tests/_output
+ support: tests/_support
+ data: tests/_data
+
+coverage:
+ enabled: true
+ include:
+ - src/ValanticSpryker/*.php
+
+modules:
+ enabled:
+ - \FondOfCodeception\Module\Spryker
+ config:
+ \FondOfCodeception\Module\Spryker:
+ generate_transfer: false
+ generate_map_classes: false
+ generate_propel_classes: false
+
+env:
+ standalone:
+ modules:
+ config:
+ \FondOfCodeception\Module\Spryker:
+ generate_transfer: true
+ generate_map_classes: true
+ generate_propel_classes: true
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..ee945b4
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,69 @@
+{
+ "name": "valantic-spryker/navigation-generator",
+ "type": "library",
+ "description": "Generates navigation-node.csv based on imported category data",
+ "require": {
+ "php": ">=8.0",
+ "spryker/kernel": "^3.0.0",
+ "spryker/store": "^1.0.0",
+ "spryker/category": "^5.0.0",
+ "spryker/category-storage": "^2.1.1"
+ },
+ "autoload": {
+ "psr-4": {
+ "ValanticSpryker\\": "src/ValanticSpryker/"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "ValanticSprykerTest\\": "tests/ValanticSprykerTest/",
+ "Generated\\": "src/Generated/",
+ "Orm\\Zed\\": "src/Orm/Zed/"
+ }
+ },
+ "minimum-stability": "dev",
+ "prefer-stable": true,
+ "config": {
+ "platform": {
+ "php": "8.0.19"
+ },
+ "preferred-install": "dist",
+ "use-include-path": true,
+ "sort-packages": true,
+ "github-protocols": [
+ "https"
+ ],
+ "process-timeout": 900,
+ "chromium-revision": 814168,
+ "allow-plugins": {
+ "dealerdirect/phpcodesniffer-composer-installer": true
+ }
+ },
+ "authors": [
+ {
+ "name": "Valantic",
+ "homepage": "https://www.valantic.com"
+ }
+ ],
+ "keywords": [
+ "spryker"
+ ],
+ "include-path": [
+ "src/"
+ ],
+ "require-dev": {
+ "fond-of-codeception/spryker": "^1.0 || ^2.0",
+ "spryker-sdk/phpstan-spryker": "*",
+ "spryker/architecture-sniffer": "*",
+ "spryker/code-sniffer": "*",
+ "spryker/development": "*",
+ "spryker/testify": "*"
+ },
+ "scripts": {
+ "cs-fix": "phpcbf --standard=phpcs.xml src",
+ "cs-check": "phpcs -s --standard=phpcs.xml --report=full src",
+ "md-check": "phpmd src/ text phpmd-ruleset.xml --minimumpriority 2",
+ "stan": "php -d memory_limit=3072M vendor/bin/phpstan analyze -l 4 src/ValanticSpryker/",
+ "test": "codecept run --env standalone --coverage-text --no-colors --coverage-html"
+ }
+}
diff --git a/phpcs.xml b/phpcs.xml
new file mode 100644
index 0000000..057c8c3
--- /dev/null
+++ b/phpcs.xml
@@ -0,0 +1,56 @@
+
+
+
+ Spryker Coding Standard for Project.
+
+ Extends main Spryker Coding Standard.
+ All sniffs in ./Sniffs will be auto loaded
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ */src/Generated/*
+ */src/Orm/*
+ */tests/_support/_generated/*
+ */tests/*/_support/_generated/*
+ */tests/*/_support/*Tester.php
+ */tests/_helpers/*
+ */tests/_output/*
+ ./data/DE/*
+ ./data/AT/*
+ ./data/SH/*
+ ./data/PZ/*
+ ./data/US/*
+ ./public
+ */node_modules/*
+ */vendor/*
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/phpmd-ruleset.xml b/phpmd-ruleset.xml
new file mode 100644
index 0000000..e1767f8
--- /dev/null
+++ b/phpmd-ruleset.xml
@@ -0,0 +1,35 @@
+
+
+
+ Extends Spryker's PHP Mess Detector Rule Set
+
+
+ tests/_data
+ tests/_output
+ tests/_support
+ */Persistence/Base/*
+ */Persistence/Map/*
+ */Orm/Propel/*
+ */Generated/*
+ *Zed/DataImport/*
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/phpstan-bootstrap.php b/phpstan-bootstrap.php
new file mode 100644
index 0000000..1abd04d
--- /dev/null
+++ b/phpstan-bootstrap.php
@@ -0,0 +1,38 @@
+locales = $this->getLocales();
+ }
+
+ /**
+ * @return void
+ */
+ public function generateNavigationNodeFile(): void
+ {
+ foreach ($this->locales as $locale) {
+ $store = explode('_', $locale)[1];
+ $categories = $this->categoryStorageClient->getCategories($locale, $store);
+
+ foreach ($categories as $category) {
+ $this->formatCategory($category, '', $locale);
+ }
+ }
+
+ $this->writeFile();
+ }
+
+ /**
+ * @param \Generated\Shared\Transfer\CategoryNodeStorageTransfer $category
+ * @param string $parentCategoryKey
+ * @param string $locale
+ *
+ * @return void
+ */
+ private function formatCategory(CategoryNodeStorageTransfer $category, string $parentCategoryKey, string $locale): void
+ {
+ $categoryKey = $this->getCategoryKey($category->getIdCategory());
+ $this->categoriesData[$locale][] = [
+ self::NODE_KEY => $categoryKey,
+ self::PARENT_NODE_KEY => $parentCategoryKey,
+ self::ATTRIBUTES => $this->getCategoryAttributes($category),
+ ];
+
+ foreach ($category->getChildren() as $child) {
+ $this->formatCategory($child, $categoryKey, $locale);
+ }
+ }
+
+ /**
+ * @return void
+ */
+ private function writeFile(): void
+ {
+ $file = fopen($this->config->getOutputPath(), 'w');
+ fputcsv($file, $this->getHeader());
+ $fallbackLocale = $this->config->getFallbackLocale();
+ foreach ($this->categoriesData[$fallbackLocale] as $key => $categoryData) {
+ $data = [
+ self::NAVIGATION_KEY => $this->config->getNavigationKey(),
+ self::NODE_KEY => $categoryData[self::NODE_KEY],
+ self::PARENT_NODE_KEY => $categoryData[self::PARENT_NODE_KEY],
+ self::NODE_TYPE => 'category',
+ ];
+
+ foreach ($this->getLocales() as $locale) {
+ $localizedCategoryData = $this->categoriesData[$locale] ?? $this->categoriesData[$fallbackLocale];
+ $data += $this->getLocalizedAttributes($localizedCategoryData[$key][self::ATTRIBUTES], $locale);
+ }
+
+ fputcsv($file, $data);
+ }
+ fclose($file);
+ }
+
+ /**
+ * @param \Generated\Shared\Transfer\CategoryNodeStorageTransfer $category
+ *
+ * @return array
+ */
+ private function getCategoryAttributes(CategoryNodeStorageTransfer $category): array
+ {
+ return [
+ self::ATTRIBUTE_TITLE => $category->getMetaTitle(),
+ self::ATTRIBUTE_URL => $category->getUrl(),
+ self::ATTRIBUTE_CSS => '',
+ ];
+ }
+
+ /**
+ * @param array $attributes
+ * @param string $locale
+ *
+ * @return array
+ */
+ private function getLocalizedAttributes(array $attributes, string $locale): array
+ {
+ $localizedAttributes = [];
+ foreach ($attributes as $attributeName => $attribute) {
+ $keyName = $attributeName . '.' . $locale;
+ $localizedAttributes[$keyName] = $attribute;
+ }
+
+ return $localizedAttributes;
+ }
+
+ /**
+ * @param int $idCategory
+ *
+ * @return string
+ */
+ private function getCategoryKey(int $idCategory): string
+ {
+ if (isset($this->categoryIdCache[$idCategory])) {
+ $categoryKey = $this->categoryIdCache[$idCategory];
+ } else {
+ $categoryKey = $this->categoryFacade->findCategoryById($idCategory)->getCategoryKey();
+ $this->categoryIdCache[$idCategory] = $categoryKey;
+ }
+
+ return $categoryKey;
+ }
+
+ /**
+ * @return string[]
+ */
+ private function getHeader(): array
+ {
+ $header = [
+ self::NAVIGATION_KEY,
+ self::NODE_KEY,
+ self::PARENT_NODE_KEY,
+ self::NODE_TYPE,
+ ];
+
+ foreach ($this->locales as $locale) {
+ $localizedHeaderFields = [
+ sprintf('%s.%s', self::ATTRIBUTE_TITLE, $locale),
+ sprintf('%s.%s', self::ATTRIBUTE_URL, $locale),
+ sprintf('%s.%s', self::ATTRIBUTE_CSS, $locale),
+ ];
+ $header = array_merge($header, $localizedHeaderFields);
+ }
+
+ return $header;
+ }
+
+ /**
+ * @return array
+ */
+ private function getLocales(): array
+ {
+ $locales = [];
+ foreach ($this->storeFacade->getAllStores() as $store) {
+ $locales = array_merge($locales, $store->getAvailableLocaleIsoCodes());
+ }
+
+ return $locales;
+ }
+}
diff --git a/src/ValanticSpryker/Zed/NavigationGenerator/Business/NavigationGeneratorBusinessFactory.php b/src/ValanticSpryker/Zed/NavigationGenerator/Business/NavigationGeneratorBusinessFactory.php
new file mode 100644
index 0000000..3e67c4d
--- /dev/null
+++ b/src/ValanticSpryker/Zed/NavigationGenerator/Business/NavigationGeneratorBusinessFactory.php
@@ -0,0 +1,55 @@
+getConfig(),
+ $this->getStoreFacade(),
+ $this->getCategoryFacade(),
+ $this->getCategoryStorageClient(),
+ );
+ }
+
+ /**
+ * @return \Spryker\Zed\Store\Business\StoreFacadeInterface
+ */
+ private function getStoreFacade(): StoreFacadeInterface
+ {
+ return $this->getProvidedDependency(NavigationGeneratorDependencyProvider::FACADE_STORE);
+ }
+
+ /**
+ * @return \ValanticSpryker\Zed\Category\Business\CategoryFacadeInterface
+ */
+ private function getCategoryFacade(): CategoryFacadeInterface
+ {
+ return $this->getProvidedDependency(NavigationGeneratorDependencyProvider::FACADE_CATEGORY);
+ }
+
+ /**
+ * @return \ValanticSpryker\Client\CategoryStorage\CategoryStorageClientInterface
+ */
+ private function getCategoryStorageClient(): CategoryStorageClientInterface
+ {
+ return $this->getProvidedDependency(NavigationGeneratorDependencyProvider::CLIENT_CATEGORY_STORAGE);
+ }
+}
diff --git a/src/ValanticSpryker/Zed/NavigationGenerator/Business/NavigationGeneratorFacade.php b/src/ValanticSpryker/Zed/NavigationGenerator/Business/NavigationGeneratorFacade.php
new file mode 100644
index 0000000..b87017d
--- /dev/null
+++ b/src/ValanticSpryker/Zed/NavigationGenerator/Business/NavigationGeneratorFacade.php
@@ -0,0 +1,21 @@
+getFactory()->createNavigationNodeFileGenerator()->generateNavigationNodeFile();
+ }
+}
diff --git a/src/ValanticSpryker/Zed/NavigationGenerator/Business/NavigationGeneratorFacadeInterface.php b/src/ValanticSpryker/Zed/NavigationGenerator/Business/NavigationGeneratorFacadeInterface.php
new file mode 100644
index 0000000..681d7fb
--- /dev/null
+++ b/src/ValanticSpryker/Zed/NavigationGenerator/Business/NavigationGeneratorFacadeInterface.php
@@ -0,0 +1,13 @@
+setName(static::COMMAND_NAME)
+ ->setDescription('Generates navigation-node.csv based on imported category data');
+ }
+
+ /**
+ * @param \Symfony\Component\Console\Input\InputInterface $input
+ * @param \Symfony\Component\Console\Output\OutputInterface $output
+ *
+ * @return int
+ */
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $this->getFacade()->generateNavigationNodeFile();
+
+ return static::CODE_SUCCESS;
+ }
+}
diff --git a/src/ValanticSpryker/Zed/NavigationGenerator/NavigationGeneratorConfig.php b/src/ValanticSpryker/Zed/NavigationGenerator/NavigationGeneratorConfig.php
new file mode 100644
index 0000000..f1bf62c
--- /dev/null
+++ b/src/ValanticSpryker/Zed/NavigationGenerator/NavigationGeneratorConfig.php
@@ -0,0 +1,35 @@
+get(NavigationGeneratorConstants::OUTPUT_PATH, APPLICATION_ROOT_DIR . '/data/import/common/common/navigation_node.csv');
+ }
+
+ /**
+ * @return string
+ */
+ public function getFallbackLocale(): string
+ {
+ return $this->get(NavigationGeneratorConstants::FALLBACK_LOCALE, 'de_DE');
+ }
+
+ /**
+ * @return string
+ */
+ public function getNavigationKey(): string
+ {
+ return $this->get(NavigationGeneratorConstants::NAVIGATION_KEY, 'MAIN_NAVIGATION');
+ }
+}
diff --git a/src/ValanticSpryker/Zed/NavigationGenerator/NavigationGeneratorDependencyProvider.php b/src/ValanticSpryker/Zed/NavigationGenerator/NavigationGeneratorDependencyProvider.php
new file mode 100644
index 0000000..3dd23d2
--- /dev/null
+++ b/src/ValanticSpryker/Zed/NavigationGenerator/NavigationGeneratorDependencyProvider.php
@@ -0,0 +1,75 @@
+addStoreFacade($container);
+ $this->addCategoryFacade($container);
+ $this->addCategoryStorageClient($container);
+
+ return $container;
+ }
+
+ /**
+ * @param \Spryker\Zed\Kernel\Container $container
+ *
+ * @return void
+ */
+ private function addStoreFacade(Container $container): void
+ {
+ $container->set(
+ static::FACADE_STORE,
+ fn (Container $container): StoreFacadeInterface => $container->getLocator()->store()->facade(),
+ );
+ }
+
+ /**
+ * @param \Spryker\Zed\Kernel\Container $container
+ *
+ * @return void
+ */
+ private function addCategoryFacade(Container $container): void
+ {
+ $container->set(
+ static::FACADE_CATEGORY,
+ fn (Container $container): CategoryFacadeInterface => $container->getLocator()->category()->facade(),
+ );
+ }
+
+ /**
+ * @param \Spryker\Zed\Kernel\Container $container
+ *
+ * @return void
+ */
+ private function addCategoryStorageClient(Container $container): void
+ {
+ $container->set(
+ static::CLIENT_CATEGORY_STORAGE,
+ fn (Container $container): CategoryStorageClientInterface => $container->getLocator()->categoryStorage()->client(),
+ );
+ }
+}
diff --git a/tests/ValanticSprykerTest/.gitkeep b/tests/ValanticSprykerTest/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/tests/_bootstrap.php b/tests/_bootstrap.php
new file mode 100644
index 0000000..641a6dd
--- /dev/null
+++ b/tests/_bootstrap.php
@@ -0,0 +1,11 @@
+