From d30696e873380058e5334bda2635e313d42c7371 Mon Sep 17 00:00:00 2001 From: Rostyslav Sabishchenko Date: Fri, 15 Dec 2017 16:48:19 +0200 Subject: [PATCH 1/5] #12717 - Catalog Products List widget is not displayed on Storefront --- .../Catalog/Model/ResourceModel/Eav/Attribute.php | 11 +++++++++++ .../CatalogWidget/Model/Rule/Condition/Product.php | 13 +++++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php b/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php index 90b206e5a6091..f713fc79231ec 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php @@ -230,6 +230,17 @@ public function afterSave() return parent::afterSave(); } + /** + * Is attribute enabled for flat indexing + * + * @return bool + */ + public function isEnabledInFlat() + { + return $this->_isEnabledInFlat(); + } + + /** * Is attribute enabled for flat indexing * diff --git a/app/code/Magento/CatalogWidget/Model/Rule/Condition/Product.php b/app/code/Magento/CatalogWidget/Model/Rule/Condition/Product.php index 9c679b8bfe9b0..9805de0bb8d34 100644 --- a/app/code/Magento/CatalogWidget/Model/Rule/Condition/Product.php +++ b/app/code/Magento/CatalogWidget/Model/Rule/Condition/Product.php @@ -119,8 +119,17 @@ public function addToCollection($collection) $attribute = $this->getAttributeObject(); if ($collection->isEnabledFlat()) { - $alias = array_keys($collection->getSelect()->getPart('from'))[0]; - $this->joinedAttributes[$attribute->getAttributeCode()] = $alias . '.' . $attribute->getAttributeCode(); + if ($attribute->isEnabledInFlat()) { + $alias = array_keys($collection->getSelect()->getPart('from'))[0]; + $this->joinedAttributes[$attribute->getAttributeCode()] = $alias . '.' . $attribute->getAttributeCode(); + } else { + $alias = 'at_' . $attribute->getAttributeCode(); + if (!in_array($alias, array_keys($collection->getSelect()->getPart('from')))) { + $collection->joinAttribute($attribute->getAttributeCode(), 'catalog_product/'.$attribute->getAttributeCode(), 'entity_id'); + } + + $this->joinedAttributes[$attribute->getAttributeCode()] = $alias . '.value'; + } return $this; } From 55a3dedb56664d5f7af22446e46aba810ce662f7 Mon Sep 17 00:00:00 2001 From: Rostyslav Sabishchenko Date: Tue, 19 Dec 2017 00:55:53 +0200 Subject: [PATCH 2/5] #12717 - Catalog Products List widget is not displayed on Storefront (code styling) --- .../Magento/Catalog/Model/ResourceModel/Eav/Attribute.php | 1 - .../Magento/CatalogWidget/Model/Rule/Condition/Product.php | 6 +++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php b/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php index f713fc79231ec..b29dba4c75665 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php @@ -240,7 +240,6 @@ public function isEnabledInFlat() return $this->_isEnabledInFlat(); } - /** * Is attribute enabled for flat indexing * diff --git a/app/code/Magento/CatalogWidget/Model/Rule/Condition/Product.php b/app/code/Magento/CatalogWidget/Model/Rule/Condition/Product.php index 9805de0bb8d34..f22879df0ae0c 100644 --- a/app/code/Magento/CatalogWidget/Model/Rule/Condition/Product.php +++ b/app/code/Magento/CatalogWidget/Model/Rule/Condition/Product.php @@ -125,7 +125,11 @@ public function addToCollection($collection) } else { $alias = 'at_' . $attribute->getAttributeCode(); if (!in_array($alias, array_keys($collection->getSelect()->getPart('from')))) { - $collection->joinAttribute($attribute->getAttributeCode(), 'catalog_product/'.$attribute->getAttributeCode(), 'entity_id'); + $collection->joinAttribute( + $attribute->getAttributeCode(), + 'catalog_product/'.$attribute->getAttributeCode(), + 'entity_id' + ); } $this->joinedAttributes[$attribute->getAttributeCode()] = $alias . '.value'; From fbe667ecd9134ce3088aabf3aa32051ba89161b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20M=C3=BCnch?= Date: Thu, 18 Jan 2018 09:58:08 +0100 Subject: [PATCH 3/5] Translate attribute label with default translation helper function Adds the translation function to customer attribute labels in Magento admin. This gives a chance to translate a label in the locale of a backend user. --- .../DataProvider/Product/Form/Modifier/EavTest.php | 14 +++++++------- .../Ui/DataProvider/Product/Form/Modifier/Eav.php | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/EavTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/EavTest.php index b91a7e1fa99fd..7f3427c356c04 100755 --- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/EavTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/EavTest.php @@ -187,7 +187,7 @@ class EavTest extends AbstractModifierTest * @var ObjectManager */ protected $objectManager; - + /** * @var Eav */ @@ -324,7 +324,7 @@ protected function setUp() $this->eavAttributeMock->expects($this->any()) ->method('load') ->willReturnSelf(); - + $this->eav =$this->getModel(); $this->objectManager->setBackwardCompatibleProperty( $this->eav, @@ -561,7 +561,7 @@ private function defaultNullProdNotNewAndRequired() 'required' => true, 'notice' => null, 'default' => null, - 'label' => null, + 'label' => new Phrase(''), 'code' => 'code', 'source' => 'product-details', 'scopeLabel' => '', @@ -588,7 +588,7 @@ private function defaultNullProdNotNewAndNotRequired() 'required' => false, 'notice' => null, 'default' => null, - 'label' => null, + 'label' => new Phrase(''), 'code' => 'code', 'source' => 'product-details', 'scopeLabel' => '', @@ -615,7 +615,7 @@ private function defaultNullProdNewAndNotRequired() 'required' => false, 'notice' => null, 'default' => 'required_value', - 'label' => null, + 'label' => new Phrase(''), 'code' => 'code', 'source' => 'product-details', 'scopeLabel' => '', @@ -642,7 +642,7 @@ private function defaultNullProdNewAndRequired() 'required' => false, 'notice' => null, 'default' => 'required_value', - 'label' => null, + 'label' => new Phrase(''), 'code' => 'code', 'source' => 'product-details', 'scopeLabel' => '', @@ -669,7 +669,7 @@ private function defaultNullProdNewAndRequiredAndFilledNotice() 'required' => false, 'notice' => __('example notice'), 'default' => 'required_value', - 'label' => null, + 'label' => new Phrase(''), 'code' => 'code', 'source' => 'product-details', 'scopeLabel' => '', diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php index 7b18bfcd4ec9c..ee6d483c9d4fb 100755 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php @@ -580,7 +580,7 @@ public function setupAttributeMeta(ProductAttributeInterface $attribute, $groupC 'required' => $attribute->getIsRequired(), 'notice' => $attribute->getNote() === null ? null : __($attribute->getNote()), 'default' => (!$this->isProductExists()) ? $attribute->getDefaultValue() : null, - 'label' => $attribute->getDefaultFrontendLabel(), + 'label' => __($attribute->getDefaultFrontendLabel()), 'code' => $attribute->getAttributeCode(), 'source' => $groupCode, 'scopeLabel' => $this->getScopeLabel($attribute), From 63c5459e56a7ad412c985544f863d2f290820f03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Mu=CC=88nch?= Date: Thu, 18 Jan 2018 15:25:41 +0100 Subject: [PATCH 4/5] Fix for missing context reset of ComponentRegistrar This test tries to setup a test environment with two modules and one theme to test the generator. During the test run, the test environment and the complete module stack is processed by the generator. So the test takes on my machine 25 seconds with xdebug enabled. After resetting the registrar the complete test runs in ~50ms with xdebug enabled. Without the reset ~12.000 files are scanned. With the reset only 5 files are scanned. This test is not a unit test. It is an integration test. --- .../DataProvider/Product/Form/Modifier/EavTest.php | 14 +++++++++----- .../Setup/Module/I18n/Dictionary/GeneratorTest.php | 2 ++ 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/EavTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/EavTest.php index 7f3427c356c04..a29379647b9e1 100755 --- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/EavTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/EavTest.php @@ -490,6 +490,10 @@ public function testSetupAttributeMetaDefaultAttribute($productId, $productRequi ->method('getNote') ->willReturn($note); + $this->productAttributeMock->expects($this->any()) + ->method('getDefaultFrontendLabel') + ->willReturn(new Phrase('mylabel')); + $attributeMock = $this->getMockBuilder(AttributeInterface::class) ->setMethods(['getValue']) ->disableOriginalConstructor() @@ -561,7 +565,7 @@ private function defaultNullProdNotNewAndRequired() 'required' => true, 'notice' => null, 'default' => null, - 'label' => new Phrase(''), + 'label' => new Phrase('mylabel'), 'code' => 'code', 'source' => 'product-details', 'scopeLabel' => '', @@ -588,7 +592,7 @@ private function defaultNullProdNotNewAndNotRequired() 'required' => false, 'notice' => null, 'default' => null, - 'label' => new Phrase(''), + 'label' => new Phrase('mylabel'), 'code' => 'code', 'source' => 'product-details', 'scopeLabel' => '', @@ -615,7 +619,7 @@ private function defaultNullProdNewAndNotRequired() 'required' => false, 'notice' => null, 'default' => 'required_value', - 'label' => new Phrase(''), + 'label' => new Phrase('mylabel'), 'code' => 'code', 'source' => 'product-details', 'scopeLabel' => '', @@ -642,7 +646,7 @@ private function defaultNullProdNewAndRequired() 'required' => false, 'notice' => null, 'default' => 'required_value', - 'label' => new Phrase(''), + 'label' => new Phrase('mylabel'), 'code' => 'code', 'source' => 'product-details', 'scopeLabel' => '', @@ -669,7 +673,7 @@ private function defaultNullProdNewAndRequiredAndFilledNotice() 'required' => false, 'notice' => __('example notice'), 'default' => 'required_value', - 'label' => new Phrase(''), + 'label' => new Phrase('mylabel'), 'code' => 'code', 'source' => 'product-details', 'scopeLabel' => '', diff --git a/dev/tests/integration/testsuite/Magento/Setup/Module/I18n/Dictionary/GeneratorTest.php b/dev/tests/integration/testsuite/Magento/Setup/Module/I18n/Dictionary/GeneratorTest.php index 7bcdb3d9fce0d..5b486f4d3bd2c 100644 --- a/dev/tests/integration/testsuite/Magento/Setup/Module/I18n/Dictionary/GeneratorTest.php +++ b/dev/tests/integration/testsuite/Magento/Setup/Module/I18n/Dictionary/GeneratorTest.php @@ -6,6 +6,7 @@ namespace Magento\Setup\Module\I18n\Dictionary; use Magento\Framework\Component\ComponentRegistrar; +use Magento\Setup\Module\I18n\Dictionary\Generator; use Magento\Setup\Module\I18n\ServiceLocator; class GeneratorTest extends \PHPUnit\Framework\TestCase @@ -46,6 +47,7 @@ protected function setUp() $paths = $reflection->getProperty('paths'); $paths->setAccessible(true); $this->backupRegistrar = $paths->getValue(); + $paths->setValue(['module' => [], 'theme' => []]); $paths->setAccessible(false); $this->testDir = realpath(__DIR__ . '/_files'); From d9111b0cac5d197962b7a098b7111f62a2c779e9 Mon Sep 17 00:00:00 2001 From: Alexander Shkurko Date: Mon, 29 Jan 2018 21:54:18 +0100 Subject: [PATCH 5/5] [Backport 2.3] #12936 out-of-stock options for configurable product visible as sellable --- .../Model/ResourceModel/Stock/Status.php | 41 ++++++++++--------- .../Model/Product/Type/Configurable.php | 23 +++++++---- .../Model/Product/Type/ConfigurableTest.php | 18 ++++---- 3 files changed, 46 insertions(+), 36 deletions(-) diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Status.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Status.php index e9f3cd59af0bb..4e04ed059c8e2 100644 --- a/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Status.php +++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Status.php @@ -5,8 +5,8 @@ */ namespace Magento\CatalogInventory\Model\ResourceModel\Stock; -use Magento\CatalogInventory\Model\Stock; use Magento\CatalogInventory\Api\StockConfigurationInterface; +use Magento\CatalogInventory\Model\Stock; use Magento\Framework\App\ObjectManager; /** @@ -46,19 +46,23 @@ class Status extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb * @param \Magento\Store\Model\WebsiteFactory $websiteFactory * @param \Magento\Eav\Model\Config $eavConfig * @param string $connectionName + * @param \Magento\CatalogInventory\Api\StockConfigurationInterface $stockConfiguration */ public function __construct( \Magento\Framework\Model\ResourceModel\Db\Context $context, \Magento\Store\Model\StoreManagerInterface $storeManager, \Magento\Store\Model\WebsiteFactory $websiteFactory, \Magento\Eav\Model\Config $eavConfig, - $connectionName = null + $connectionName = null, + $stockConfiguration = null ) { parent::__construct($context, $connectionName); $this->_storeManager = $storeManager; $this->_websiteFactory = $websiteFactory; $this->eavConfig = $eavConfig; + $this->stockConfiguration = $stockConfiguration ?: ObjectManager::getInstance() + ->get(StockConfigurationInterface::class); } /** @@ -204,7 +208,7 @@ public function getProductCollection($lastEntityId = 0, $limit = 1000) */ public function addStockStatusToSelect(\Magento\Framework\DB\Select $select, \Magento\Store\Model\Website $website) { - $websiteId = $this->getStockConfiguration()->getDefaultScopeId(); + $websiteId = $this->getWebsiteId($website->getId()); $select->joinLeft( ['stock_status' => $this->getMainTable()], 'e.entity_id = stock_status.product_id AND stock_status.website_id=' . $websiteId, @@ -221,7 +225,7 @@ public function addStockStatusToSelect(\Magento\Framework\DB\Select $select, \Ma */ public function addStockDataToCollection($collection, $isFilterInStock) { - $websiteId = $this->getStockConfiguration()->getDefaultScopeId(); + $websiteId = $this->getWebsiteId(); $joinCondition = $this->getConnection()->quoteInto( 'e.entity_id = stock_status_index.product_id' . ' AND stock_status_index.website_id = ?', $websiteId @@ -255,7 +259,7 @@ public function addStockDataToCollection($collection, $isFilterInStock) */ public function addIsInStockFilterToCollection($collection) { - $websiteId = $this->getStockConfiguration()->getDefaultScopeId(); + $websiteId = $this->getWebsiteId(); $joinCondition = $this->getConnection()->quoteInto( 'e.entity_id = stock_status_index.product_id' . ' AND stock_status_index.website_id = ?', $websiteId @@ -277,6 +281,19 @@ public function addIsInStockFilterToCollection($collection) return $this; } + /** + * @param \Magento\Store\Model\Website $websiteId + * @return int + */ + private function getWebsiteId($websiteId = null) + { + if (null === $websiteId) { + $websiteId = $this->stockConfiguration->getDefaultScopeId(); + } + + return $websiteId; + } + /** * Retrieve Product(s) status for store * Return array where key is a product_id, value - status @@ -335,18 +352,4 @@ public function getProductStatus($productIds, $storeId = null) } return $statuses; } - - /** - * @return StockConfigurationInterface - * - * @deprecated 100.1.0 - */ - private function getStockConfiguration() - { - if ($this->stockConfiguration === null) { - $this->stockConfiguration = \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\CatalogInventory\Api\StockConfigurationInterface::class); - } - return $this->stockConfiguration; - } } diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php index e6345af40f37a..29583231a764a 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php @@ -3,17 +3,18 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\ConfigurableProduct\Model\Product\Type; use Magento\Catalog\Api\Data\ProductAttributeInterface; use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Api\Data\ProductInterfaceFactory; use Magento\Catalog\Api\ProductRepositoryInterface; -use Magento\ConfigurableProduct\Model\Product\Type\Collection\SalableProcessor; use Magento\Catalog\Model\Config; +use Magento\Catalog\Model\Product\Gallery\ReadHandler as GalleryReadHandler; +use Magento\ConfigurableProduct\Model\Product\Type\Collection\SalableProcessor; use Magento\Framework\App\ObjectManager; use Magento\Framework\EntityManager\MetadataPool; -use Magento\Catalog\Model\Product\Gallery\ReadHandler as GalleryReadHandler; /** * Configurable product type implementation @@ -266,7 +267,6 @@ public function __construct( $productRepository, $serializer ); - } /** @@ -682,7 +682,7 @@ private function saveConfigurableOptions(ProductInterface $product) ->setProductId($product->getData($metadata->getLinkField())) ->save(); } - /** @var $configurableAttributesCollection \Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Attribute\Collection */ + /** @var $configurableAttributesCollection \Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Attribute\Collection */ $configurableAttributesCollection = $this->_attributeCollectionFactory->create(); $configurableAttributesCollection->setProductFilter($product); $configurableAttributesCollection->addFieldToFilter( @@ -1276,6 +1276,8 @@ public function getSalableUsedProducts(\Magento\Catalog\Model\Product $product, * Load collection on sub-products for specified configurable product * * Load collection of sub-products, apply result to specified configurable product and store result to cache + * Please note $salableOnly parameter is used for backwards compatibility because of deprecated method + * getSalableUsedProducts * Number of loaded sub-products depends on $salableOnly parameter * $salableOnly = true - result array contains only salable sub-products * $salableOnly = false - result array contains all sub-products @@ -1292,7 +1294,7 @@ private function loadUsedProducts(\Magento\Catalog\Model\Product $product, $cach if (!$product->hasData($dataFieldName)) { $usedProducts = $this->readUsedProductsCacheData($cacheKey); if ($usedProducts === null) { - $collection = $this->getConfiguredUsedProductCollection($product); + $collection = $this->getConfiguredUsedProductCollection($product, false); if ($salableOnly) { $collection = $this->salableProcessor->process($collection); } @@ -1386,13 +1388,18 @@ private function getUsedProductsCacheKey($keyParts) * Retrieve related products collection with additional configuration * * @param \Magento\Catalog\Model\Product $product + * @param bool $skipStockFilter * @return \Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Product\Collection */ - private function getConfiguredUsedProductCollection(\Magento\Catalog\Model\Product $product) - { + private function getConfiguredUsedProductCollection( + \Magento\Catalog\Model\Product $product, + $skipStockFilter = true + ) { $collection = $this->getUsedProductCollection($product); + if ($skipStockFilter) { + $collection->setFlag('has_stock_status_filter', true); + } $collection - ->setFlag('has_stock_status_filter', true) ->addAttributeToSelect($this->getCatalogConfig()->getProductAttributes()) ->addFilterByRequiredOptions() ->setStoreId($product->getStoreId()); diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/ConfigurableTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/ConfigurableTest.php index 7bfa78e92349c..5e9399ddd3d65 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/ConfigurableTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/ConfigurableTest.php @@ -8,20 +8,20 @@ use Magento\Catalog\Api\Data\ProductExtensionInterface; use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\Config; -use Magento\ConfigurableProduct\Model\Product\Type\Configurable; -use Magento\Framework\EntityManager\EntityMetadata; -use Magento\Framework\EntityManager\MetadataPool; -use Magento\Customer\Model\Session; -use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Attribute\CollectionFactory; -use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Attribute\Collection; use Magento\Catalog\Model\Product\Configuration\Item\Option\OptionInterface; -use Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface; +use Magento\ConfigurableProduct\Model\Product\Type\Collection\SalableProcessor; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable; use Magento\ConfigurableProduct\Model\Product\Type\Configurable\AttributeFactory; +use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Attribute\Collection; +use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Attribute\CollectionFactory; +use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Product\Collection as ProductCollection; use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\ConfigurableFactory; +use Magento\Customer\Model\Session; use Magento\Eav\Model\Entity\Attribute\Frontend\AbstractFrontend; use Magento\Eav\Model\Entity\Attribute\Source\AbstractSource; -use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Product\Collection as ProductCollection; -use Magento\ConfigurableProduct\Model\Product\Type\Collection\SalableProcessor; +use Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface; +use Magento\Framework\EntityManager\EntityMetadata; +use Magento\Framework\EntityManager\MetadataPool; /** * @SuppressWarnings(PHPMD.LongVariable)