diff --git a/.gitignore b/.gitignore
index ffd2a4a..f703568 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,3 @@
*.idea
+Port1Typo3Connector/
+Port1Typo3Connector-*.zip
diff --git a/Components/Api/Resource/Variant.php b/Components/Api/Resource/Variant.php
new file mode 100644
index 0000000..1e4ca35
--- /dev/null
+++ b/Components/Api/Resource/Variant.php
@@ -0,0 +1,612 @@
+, portrino GmbH
+ */
+
+use Shopware\Bundle\StoreFrontBundle\Struct\Media;
+use Shopware\Bundle\StoreFrontBundle\Struct\Product;
+use Shopware\Components\Api\Exception as ApiException;
+use Shopware\Components\Api\Resource\Translation;
+use Shopware\Components\Model\QueryBuilder;
+use Shopware\Models\Article\Detail;
+use Shopware\Models\Shop\Shop;
+
+/**
+ * Class Article
+ *
+ * @package Port1Typo3Connector\Components\Api\Resource
+ */
+class Variant extends \Shopware\Components\Api\Resource\Variant
+{
+
+ /**
+ * @param int $id
+ * @param array $options
+ *
+ * @return array|Detail
+ * @throws ApiException\CustomValidationException
+ * @throws ApiException\NotFoundException
+ * @throws ApiException\ParameterMissingException
+ * @throws ApiException\PrivilegeException
+ * @throws \Doctrine\ORM\NonUniqueResultException
+ */
+ public function getOne($id, array $options = [])
+ {
+ $this->checkPrivilege('read');
+
+ if (empty($id)) {
+ throw new ApiException\ParameterMissingException();
+ }
+
+ $builder = $this->getRepository()->createQueryBuilder('detail')
+ ->addSelect([
+ 'prices',
+ 'attribute',
+ 'partial article.{id,name,description,descriptionLong,active,taxId,changed}',
+ 'customerGroup',
+ 'options',
+ 'images'
+ ])
+ ->leftJoin('detail.prices', 'prices')
+ ->innerJoin('prices.customerGroup', 'customerGroup')
+ ->leftJoin('detail.attribute', 'attribute')
+ ->innerJoin('detail.article', 'article')
+ ->leftJoin('article.images', 'images')
+ ->leftJoin('detail.configuratorOptions', 'options');
+
+ $builder->andWhere('detail.id = :variantId')
+ ->addOrderBy('detail.id', 'ASC')
+ ->addOrderBy('customerGroup.id', 'ASC')
+ ->addOrderBy('prices.from', 'ASC')
+ ->setParameter('variantId', $id);
+
+ /** @var Detail|array $variant */
+ $variant = $builder->getQuery()->getOneOrNullResult($this->getResultMode());
+
+ if (!$variant) {
+ throw new ApiException\NotFoundException(sprintf('Variant by id %d not found', $id));
+ }
+
+ if (($this->getResultMode() === self::HYDRATE_ARRAY)
+ && isset($options['considerTaxInput'])
+ && $options['considerTaxInput']
+ ) {
+ $variant = $this->considerTaxInput($variant);
+ }
+
+ try {
+ $frontController = Shopware()->Front();
+ if ($frontController) {
+ $params = $frontController->Request()->getParams();
+ if (!array_key_exists('language', $options) && array_key_exists('language', $params)) {
+ $options['language'] = $params['language'];
+ }
+ }
+ } catch (\Exception $e) {
+ // ...
+ }
+
+ if ($this->getResultMode() === self::HYDRATE_ARRAY
+ && isset($options['language'])
+ && !empty($options['language'])) {
+ /** @var Shop $shop */
+ $shop = $this->findEntityByConditions(Shop::class, [
+ ['id' => $options['language']],
+ ]);
+
+ $variant['article'] = $this->translateArticle($variant['article'], $shop);
+
+ /** @var \Shopware\Bundle\StoreFrontBundle\Service\ProductServiceInterface $contextService */
+ $productService = $this->container->get('shopware_storefront.product_service');
+ /** @var \Shopware\Bundle\StoreFrontBundle\Service\ContextServiceInterface $contextService */
+ $contextService = $this->container->get('shopware_storefront.context_service');
+ /** @var Product $product */
+ $product = $productService->get($variant['number'], $contextService->createShopContext($shop->getId()));
+
+ $variant['article']['images'] = $this->getSortedArticleImages($variant['article']['images'], $product);
+ }
+
+ return $variant;
+ }
+
+ /**
+ * @param int $offset
+ * @param int $limit
+ * @param array $criteria
+ * @param array $orderBy
+ * @param array $options
+ *
+ * @return array
+ * @throws ApiException\CustomValidationException
+ * @throws ApiException\PrivilegeException
+ * @throws \Exception
+ */
+ public function getList($offset = 0, $limit = 25, array $criteria = [], array $orderBy = [], array $options = [])
+ {
+ $this->checkPrivilege('read');
+
+ /** @var QueryBuilder $builder */
+ $builder = $this->getRepository()->createQueryBuilder('detail')
+ ->addSelect([
+ 'prices',
+ 'attribute',
+ 'partial article.{id,name,description,descriptionLong,active,taxId,changed}',
+ 'customerGroup',
+ 'images'
+ ])
+ ->leftJoin('detail.prices', 'prices')
+ ->innerJoin('prices.customerGroup', 'customerGroup')
+ ->leftJoin('detail.attribute', 'attribute')
+ ->innerJoin('detail.article', 'article')
+ ->leftJoin('article.images', 'images')
+ ->addFilter($criteria)
+ ->addOrderBy($orderBy)
+ ->setFirstResult($offset)
+ ->setMaxResults($limit);
+
+ $query = $builder->getQuery();
+
+ $query->setHydrationMode($this->getResultMode());
+
+ $paginator = $this->getManager()->createPaginator($query);
+
+ // Returns the total count of the query
+ $totalResult = $paginator->count();
+
+ // Returns the product data
+ $variants = $paginator->getIterator()->getArrayCopy();
+
+ if (($this->getResultMode() === self::HYDRATE_ARRAY)
+ && isset($options['considerTaxInput'])
+ && $options['considerTaxInput']
+ ) {
+ foreach ($variants as &$variant) {
+ $variant = $this->considerTaxInput($variant);
+ }
+ unset($variant);
+ }
+
+ try {
+ $frontController = Shopware()->Front();
+ if ($frontController) {
+ $params = $frontController->Request()->getParams();
+ if (!array_key_exists('language', $options) && array_key_exists('language', $params)) {
+ $options['language'] = $params['language'];
+ }
+ }
+ } catch (\Exception $e) {
+ // ...
+ }
+
+ if ($this->getResultMode() === self::HYDRATE_ARRAY
+ && isset($options['language'])
+ && !empty($options['language'])) {
+ /** @var Shop $shop */
+ $shop = $this->findEntityByConditions(Shop::class, [
+ ['id' => $options['language']],
+ ]);
+
+ /** @var array $variant */
+ foreach ($variants as &$variant) {
+ $variant['article'] = $this->translateArticle($variant['article'], $shop);
+
+ /** @var \Shopware\Bundle\StoreFrontBundle\Service\ProductServiceInterface $contextService */
+ $productService = $this->container->get('shopware_storefront.product_service');
+ /** @var \Shopware\Bundle\StoreFrontBundle\Service\ContextServiceInterface $contextService */
+ $contextService = $this->container->get('shopware_storefront.context_service');
+ /** @var Product $product */
+ $product = $productService->get($variant['number'], $contextService->createShopContext($shop->getId()));
+
+ $variant['article']['images'] = $this->getSortedArticleImages($variant['article']['images'], $product);
+ }
+ unset($variant);
+ $this->translateVariants($variants, $shop);
+ }
+
+ return ['data' => $variants, 'total' => $totalResult];
+ }
+
+ /**
+ * @param array $variant
+ *
+ * @return array
+ * @throws ApiException\CustomValidationException
+ *
+ */
+ private function considerTaxInput(array $variant)
+ {
+ $tax = Shopware()->Db()->fetchOne(
+ 'SELECT tax
+ FROM s_core_tax
+ INNER JOIN s_articles
+ ON s_articles.taxID = s_core_tax.id
+ AND s_articles.id = :articleId',
+ [':articleId' => $variant['articleId']]
+ );
+
+ if (empty($tax)) {
+ throw new ApiException\CustomValidationException(
+ sprintf('No product tax configured for variant: %s', $variant['id'])
+ );
+ }
+
+ $variant['prices'] = $this->getArticleResource()->getTaxPrices(
+ $variant['prices'],
+ $tax
+ );
+
+ return $variant;
+ }
+
+ /**
+ * @return Translation
+ */
+ protected function getTranslationResource()
+ {
+ /** @var Translation $return */
+ $return = $this->getResource('Translation');
+
+ return $return;
+ }
+
+ /**
+ * Translate the whole product array.
+ *
+ * @param array $data
+ * @param Shop $shop
+ *
+ * @return array
+ */
+ protected function translateArticle(array $data, Shop $shop)
+ {
+ $this->getTranslationResource()->setResultMode(
+ self::HYDRATE_ARRAY
+ );
+ $translation = $this->getSingleTranslation(
+ 'article',
+ $shop->getId(),
+ $data['id']
+ );
+
+ if (!empty($translation)) {
+ $data = $this->mergeTranslation($data, $translation['data']);
+
+ if ($data['mainDetail']) {
+ $data['mainDetail'] = $this->mergeTranslation($data['mainDetail'], $translation['data']);
+
+ if ($data['mainDetail']['attribute']) {
+ $data['mainDetail']['attribute'] = $this->mergeTranslation(
+ $data['mainDetail']['attribute'],
+ $translation['data']
+ );
+ }
+
+ if ($data['mainDetail']['configuratorOptions']) {
+ $data['mainDetail']['configuratorOptions'] = $this->translateAssociation(
+ $data['mainDetail']['configuratorOptions'],
+ $shop,
+ 'configuratoroption'
+ );
+ }
+ }
+ }
+
+ $data['details'] = $this->translateVariants(
+ $data['details'],
+ $shop
+ );
+
+ if (isset($data['links'])) {
+ $data['links'] = $this->translateAssociation(
+ $data['links'],
+ $shop,
+ 'link'
+ );
+ }
+
+ if (isset($data['downloads'])) {
+ $data['downloads'] = $this->translateAssociation(
+ $data['downloads'],
+ $shop,
+ 'download'
+ );
+ }
+
+ $data['supplier'] = $this->translateSupplier($data['supplier'], $shop);
+
+ $data['propertyValues'] = $this->translatePropertyValues($data['propertyValues'], $shop);
+
+ $data['propertyGroup'] = $this->translatePropertyGroup($data['propertyGroup'], $shop);
+
+ if (!empty($data['configuratorSet']) && !empty($data['configuratorSet']['groups'])) {
+ $data['configuratorSet']['groups'] = $this->translateAssociation(
+ $data['configuratorSet']['groups'],
+ $shop,
+ 'configuratorgroup'
+ );
+ }
+
+ if (isset($data['related'])) {
+ $data['related'] = $this->translateAssociation(
+ $data['related'],
+ $shop,
+ 'article'
+ );
+ }
+
+ if (isset($data['similar'])) {
+ $data['similar'] = $this->translateAssociation(
+ $data['similar'],
+ $shop,
+ 'article'
+ );
+ }
+
+ if (isset($data['images'])) {
+ $data['images'] = $this->translateAssociation(
+ $data['images'],
+ $shop,
+ 'articleimage'
+ );
+ }
+
+ return $data;
+ }
+
+ /**
+ * Translates the passed values array with the passed shop entity.
+ *
+ * @param array $values
+ * @param Shop $shop
+ *
+ * @return mixed
+ */
+ protected function translatePropertyValues($values, Shop $shop)
+ {
+ if (empty($values)) {
+ return $values;
+ }
+
+ foreach ($values as &$value) {
+ $translation = $this->getSingleTranslation(
+ 'propertyvalue',
+ $shop->getId(),
+ $value['id']
+ );
+ if (empty($translation)) {
+ continue;
+ }
+
+ $translation['data']['value'] = $translation['data']['optionValue'];
+
+ $value = $this->mergeTranslation(
+ $value,
+ $translation['data']
+ );
+ }
+
+ return $values;
+ }
+
+ /**
+ * Translates the passed supplier data.
+ *
+ * @param array $supplier
+ * @param Shop $shop
+ *
+ * @return array
+ */
+ protected function translateSupplier($supplier, Shop $shop)
+ {
+ if (empty($supplier)) {
+ return $supplier;
+ }
+ $translation = $this->getSingleTranslation(
+ 'supplier',
+ $shop->getId(),
+ $supplier['id']
+ );
+
+ if (empty($translation)) {
+ return $supplier;
+ }
+
+ return $this->mergeTranslation(
+ $supplier,
+ $translation['data']
+ );
+ }
+
+ /**
+ * Translates the passed property group data.
+ *
+ * @param array $groupData
+ * @param Shop $shop
+ *
+ * @return array
+ */
+ protected function translatePropertyGroup($groupData, Shop $shop)
+ {
+ if (empty($groupData)) {
+ return $groupData;
+ }
+
+ $translation = $this->getSingleTranslation(
+ 'propertygroup',
+ $shop->getId(),
+ $groupData['id']
+ );
+
+ if (empty($translation)) {
+ return $groupData;
+ }
+
+ $translation['data']['name'] = $translation['data']['groupName'];
+
+ return $this->mergeTranslation(
+ $groupData,
+ $translation['data']
+ );
+ }
+
+ /**
+ * Translates the passed variants array and all associated data.
+ *
+ * @param array $details
+ * @param Shop $shop
+ *
+ * @return mixed
+ */
+ protected function translateVariants($details, Shop $shop)
+ {
+ if (empty($details)) {
+ return $details;
+ }
+
+ foreach ($details as &$variant) {
+ $translation = $this->getSingleTranslation(
+ 'variant',
+ $shop->getId(),
+ $variant['id']
+ );
+ if (empty($translation)) {
+ continue;
+ }
+ $variant = $this->mergeTranslation(
+ $variant,
+ $translation['data']
+ );
+ $variant['attribute'] = $this->mergeTranslation(
+ $variant['attribute'],
+ $translation['data']
+ );
+
+ if ($variant['configuratorOptions']) {
+ $variant['configuratorOptions'] = $this->translateAssociation(
+ $variant['configuratorOptions'],
+ $shop,
+ 'configuratoroption'
+ );
+ }
+
+ if ($variant['images']) {
+ foreach ($variant['images'] as &$image) {
+ $translation = $this->getSingleTranslation(
+ 'articleimage',
+ $shop->getId(),
+ $image['parentId']
+ );
+ if (empty($translation)) {
+ continue;
+ }
+ $image = $this->mergeTranslation($image, $translation['data']);
+ }
+ }
+ }
+
+ return $details;
+ }
+
+ /**
+ * Helper function which merges the translated data into the already
+ * existing data object. This function merges only values, which already
+ * exist in the original data array.
+ *
+ * @param array $data
+ * @param array $translation
+ *
+ * @return array
+ */
+ protected function mergeTranslation($data, $translation)
+ {
+ $data = array_merge(
+ $data,
+ array_intersect_key($translation, $data)
+ );
+
+ return $data;
+ }
+
+ /**
+ * Helper function which translates associated array data.
+ *
+ * @param array $association
+ * @param Shop $shop
+ * @param string $type
+ *
+ * @return array
+ */
+ protected function translateAssociation(array $association, Shop $shop, $type)
+ {
+ foreach ($association as &$item) {
+ $translation = $this->getSingleTranslation(
+ $type,
+ $shop->getId(),
+ $item['id']
+ );
+ if (empty($translation)) {
+ continue;
+ }
+ $item = $this->mergeTranslation($item, $translation['data']);
+ }
+
+ return $association;
+ }
+
+ /**
+ * Helper function to get a single translation.
+ *
+ * @param string $type
+ * @param int $shopId
+ * @param string $key
+ *
+ * @return array
+ */
+ protected function getSingleTranslation($type, $shopId, $key)
+ {
+ $translation = $this->getTranslationResource()->getList(0, 1, [
+ ['property' => 'translation.type', 'value' => $type],
+ ['property' => 'translation.key', 'value' => $key],
+ ['property' => 'translation.shopId', 'value' => $shopId],
+ ]);
+
+ return $translation['data'][0];
+ }
+
+ /**
+ * @param array $images
+ * @param Product $product
+ * @return array
+ */
+ private function getSortedArticleImages($images, $product)
+ {
+ $result = [];
+
+ if ($product->getCover()) {
+ foreach ($images as $image) {
+ if ($image['mediaId'] === $product->getCover()->getId()) {
+ $result[] = $image;
+ }
+ }
+ }
+ if ($product->getMedia()) {
+ /** @var Media $media */
+ foreach ($product->getMedia() as $media) {
+ foreach ($images as $image) {
+ if ($image['mediaId'] === $media->getId()) {
+ if (!$product->getCover() || ($product->getCover() && $media->getId() !== $product->getCover()->getId())) {
+ $result[] = $image;
+ }
+ }
+ }
+ }
+ }
+
+ return $result;
+ }
+}
diff --git a/Components/ApiUrlDecorator.php b/Components/ApiUrlDecorator.php
index d416111..54c29c8 100644
--- a/Components/ApiUrlDecorator.php
+++ b/Components/ApiUrlDecorator.php
@@ -186,8 +186,9 @@ public function addUrl()
/**
* @param int $itemId
+ * @param string $orderNumber
*
* @return string
*/
- abstract protected function getItemUrl($itemId);
+ abstract protected function getItemUrl($itemId, $orderNumber = null);
}
diff --git a/Components/ApiUrlDecorator/ApiArticlesUrlDecorator.php b/Components/ApiUrlDecorator/ApiArticlesUrlDecorator.php
index c628195..2491cb1 100644
--- a/Components/ApiUrlDecorator/ApiArticlesUrlDecorator.php
+++ b/Components/ApiUrlDecorator/ApiArticlesUrlDecorator.php
@@ -14,10 +14,11 @@ class ApiArticlesUrlDecorator extends ApiUrlDecorator
/**
* @param int $itemId
+ * @param string $orderNumber
* @return null|string
* @throws \Exception
*/
- protected function getItemUrl($itemId)
+ protected function getItemUrl($itemId, $orderNumber = null)
{
$arr = [
'sViewport' => 'detail',
diff --git a/Components/ApiUrlDecorator/ApiCategoriesUrlDecorator.php b/Components/ApiUrlDecorator/ApiCategoriesUrlDecorator.php
index 50c945d..6d07a18 100644
--- a/Components/ApiUrlDecorator/ApiCategoriesUrlDecorator.php
+++ b/Components/ApiUrlDecorator/ApiCategoriesUrlDecorator.php
@@ -14,10 +14,11 @@ class ApiCategoriesUrlDecorator extends ApiUrlDecorator
/**
* @param int $itemId
+ * @param string $orderNumber
* @return null|string
* @throws \Exception
*/
- protected function getItemUrl($itemId)
+ protected function getItemUrl($itemId, $orderNumber = null)
{
$arr = [
'sViewport' => 'cat',
diff --git a/Components/ApiUrlDecorator/ApiVariantsUrlDecorator.php b/Components/ApiUrlDecorator/ApiVariantsUrlDecorator.php
new file mode 100644
index 0000000..92871d7
--- /dev/null
+++ b/Components/ApiUrlDecorator/ApiVariantsUrlDecorator.php
@@ -0,0 +1,103 @@
+isPxShopwareRequest) {
+ try {
+ $dataBefore = $this->controller->View()->getAssign('data');
+
+ $data = $this->controller->View()->getAssign('data');
+ $action = $this->controller->Request()->getActionName();
+
+ if ($action === 'get') {
+ if (is_array($data) && isset($data['articleId'])) {
+ $url = $this->getItemUrl($data['articleId'], $data['number']);
+ $data = array_merge_recursive(
+ $this->controller->View()->getAssign('data'),
+ ['pxShopwareUrl' => $url]
+ );
+ }
+ }
+
+ if ($action === 'index') {
+ if (is_array($data)) {
+ // add article urls to each article of the list
+ $items = $this->controller->View()->getAssign('data');
+ foreach ($items as $key => $item) {
+ $item['pxShopwareUrl'] = $this->getItemUrl($item['articleId'], $item['number']);
+ $items[$key] = $item;
+ }
+ $data = $items;
+ }
+
+ }
+
+ $this->controller->View()->clearAssign('data');
+ $this->controller->View()->assign('data', $data);
+
+ /**
+ * we have to call postDispatch again to force Zend_Json::encode call
+ */
+ $this->controller->postDispatch();
+
+ } catch (\Exception $exception) {
+ /**
+ * in case of an error we should reset data and render view again with data before
+ */
+ $this->controller->View()->clearAssign('data');
+ $this->controller->View()->assign('data', $dataBefore);
+ $this->controller->postDispatch();
+ }
+ }
+ }
+
+ /**
+ * @param int $itemId
+ * @param string $orderNumber
+ * @return null|string
+ * @throws \Exception
+ */
+ protected function getItemUrl($itemId, $orderNumber = null)
+ {
+ $arr = [
+ 'sViewport' => 'detail',
+ 'sArticle' => $itemId,
+ 'module' => 'frontend',
+ 'forceSecure' => true,
+ 'number' => $orderNumber,
+ ];
+ $result = null;
+ $router = $this->controller->Front()->Router();
+ if ($router instanceof Router) {
+ $url = $router->assemble($arr);
+ if ($url !== false) {
+ if ($router->getContext()->isUrlToLower()) {
+ if (strpos($url, '?') !== false) {
+ list($uri, $params) = explode('?', $url);
+ $url = strtolower($uri) . '?' . $params;
+ } else {
+ $url = strtolower($url);
+ }
+ }
+ $result = $url;
+ }
+ }
+ return $result;
+ }
+
+}
\ No newline at end of file
diff --git a/Port1Typo3Connector.php b/Port1Typo3Connector.php
index 55c467d..28a7ef1 100644
--- a/Port1Typo3Connector.php
+++ b/Port1Typo3Connector.php
@@ -1,10 +1,12 @@
addUrl();
}
+ /**
+ * @param \Enlight_Event_EventArgs $args
+ * @throws \Exception
+ */
+ public function onApiVariantsAddUrl(\Enlight_Event_EventArgs $args)
+ {
+ $apiUrlDecorator = new ApiVariantsUrlDecorator($args->get('subject'), $this->container);
+ $apiUrlDecorator->addUrl();
+ }
+
/**
* @param \Enlight_Event_EventArgs $args
*/
diff --git a/Resources/services.xml b/Resources/services.xml
index 752e05c..574d441 100644
--- a/Resources/services.xml
+++ b/Resources/services.xml
@@ -1,20 +1,21 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/build.sh b/build.sh
new file mode 100755
index 0000000..5f6fa10
--- /dev/null
+++ b/build.sh
@@ -0,0 +1,17 @@
+#!/usr/bin/env bash
+
+commit=$1
+if [ -z ${commit} ]; then
+ commit=$(git tag --sort=-creatordate | head -1)
+ if [ -z ${commit} ]; then
+ commit="master";
+ fi
+fi
+
+# Remove old release
+rm -rf Port1Typo3Connector/ Port1Typo3Connector-*.zip
+
+# Build new release
+mkdir -p Port1Typo3Connector
+git archive ${commit} | tar -x -C Port1Typo3Connector
+zip -r Port1Typo3Connector-${commit}.zip Port1Typo3Connector
diff --git a/plugin.xml b/plugin.xml
index 1812ed6..ae13fac 100644
--- a/plugin.xml
+++ b/plugin.xml
@@ -3,7 +3,7 @@
- 2.3.0
+ 2.4.0
© 2019 by portrino GmbH
@@ -16,7 +16,7 @@
Ermöglicht die Kommunikation mit der TYPO3-Erweiterung "PxShopware"
Enables communication with TYPO3-Extension "PxShopware".
-
+
[TASK] überschreibt die API Media Resource und fügt Thumbnails hinzu
@@ -25,6 +25,15 @@
+
+
+ [TASK] Unterstützung für Varianten-Artikel
+
+
+ [TASK] Adds support for article variants
+
+
+
[TASK] Fügt Versionsbeschränkung für in Shopware 5.4 veraltete Methodenaufrufe hinzu