From fc06fbe77bc18c89339ee38c721243e28830fa46 Mon Sep 17 00:00:00 2001 From: Oleksandr Kravchuk Date: Sat, 13 Nov 2021 14:11:16 +0200 Subject: [PATCH] run-as-root/magento2-prometheus-exporter#24: - add categories count aggregator based on category status, category menu status and store code - add unit tests coverage for categories count aggregator. --- .../Category/CategoryCountAggregatorTest.php | 275 ++++++++++++++++++ .../Category/CategoryCountAggregator.php | 197 +++++++++++++ .../Shipment/ShipmentCountAggregator.php | 2 +- src/etc/di.xml | 3 + 4 files changed, 476 insertions(+), 1 deletion(-) create mode 100644 Test/Unit/Aggregator/Category/CategoryCountAggregatorTest.php create mode 100644 src/Aggregator/Category/CategoryCountAggregator.php diff --git a/Test/Unit/Aggregator/Category/CategoryCountAggregatorTest.php b/Test/Unit/Aggregator/Category/CategoryCountAggregatorTest.php new file mode 100644 index 0000000..63de4a6 --- /dev/null +++ b/Test/Unit/Aggregator/Category/CategoryCountAggregatorTest.php @@ -0,0 +1,275 @@ +updateMetricService = $this->createMock(UpdateMetricService::class); + $this->resourceConnection = $this->createMock(ResourceConnection::class); + $this->expressionFactory = $this->createMock(ExpressionFactory::class); + $this->metadataPool = $this->createMock(MetadataPool::class); + + $this->subject = new CategoryCountAggregator( + $this->updateMetricService, + $this->resourceConnection, + $this->metadataPool, + $this->expressionFactory + ); + } + + private function getStatisticData(): array + { + return [ + [ + 'STORE_CODE' => 'default', + 'ACTIVE_IN_MENU' => 50, + 'ACTIVE_NOT_IN_MENU' => 10, + 'NOT_ACTIVE_IN_MENU' => 25, + 'NOT_ACTIVE_NOT_IN_MENU' => 15 + + ], + [ + 'STORE_CODE' => 'base', + 'ACTIVE_IN_MENU' => 18, + 'ACTIVE_NOT_IN_MENU' => 3, + 'NOT_ACTIVE_IN_MENU' => 4, + 'NOT_ACTIVE_NOT_IN_MENU' => 1 + + ], + [ + 'STORE_CODE' => 'eu', + 'ACTIVE_IN_MENU' => 79, + 'ACTIVE_NOT_IN_MENU' => 21, + 'NOT_ACTIVE_IN_MENU' => 15, + 'NOT_ACTIVE_NOT_IN_MENU' => 16 + + ], + ]; + } + + private function getSelectMock(): MockObject + { + $select = $this->createMock(Select::class); + + $select->expects($this->exactly(3))->method('from') + ->withConsecutive([self::T_ATT], [self::T_ATT], [["sg" => self::T_STORE_GROUP]])->willReturn($select); + + $select->expects($this->exactly(4))->method('where') + ->withConsecutive( + ['entity_type_id = ?', 3], + ['attribute_code = ?', 'is_active'], + ['entity_type_id = ?', 3], + ['attribute_code = ?', 'include_in_menu'], + )->willReturn($select); + $select->expects($this->exactly(3)) + ->method('reset') + ->with(Select::COLUMNS) + ->willReturn($select); + + $select->expects($this->exactly(3))->method('joinInner') + ->withConsecutive( + [ + ['s' => self::T_STORE], + 'sg.group_id = s.group_id' + ], + [ + ['cce1' => self::T_CAT_ENT], + 'sg.root_category_id = cce1.entity_id' + ], + [ + ['cce2' => self::T_CAT_ENT], + "cce2.path like CONCAT(cce1.path, '%')" + ] + )->willReturn($select); + + $select->expects($this->exactly(4))->method('joinLeft') + ->withConsecutive( + [ + ['ccei1' => self::T_CAT_ENT_INT], + sprintf( + "cce2.%s = ccei1.%s AND ccei1.attribute_id = %s AND ccei1.store_id = s.store_id", + self::LINK_FIELD, + self::LINK_FIELD, + self::ATTR_ID + ) + ], + [ + ['ccei2' => self::T_CAT_ENT_INT], + sprintf( + "cce2.%s = ccei2.%s AND ccei2.attribute_id = %s AND ccei2.store_id = 0", + self::LINK_FIELD, + self::LINK_FIELD, + self::ATTR_ID + ) + ], + [ + ['ccei3' => self::T_CAT_ENT_INT], + sprintf( + "cce2.%s = ccei3.%s AND ccei3.attribute_id = %s AND ccei3.store_id = s.store_id", + self::LINK_FIELD, + self::LINK_FIELD, + self::ATTR_ID + ) + ], + [ + ['ccei4' => self::T_CAT_ENT_INT], + sprintf( + "cce2.%s = ccei4.%s AND ccei4.attribute_id = %s AND ccei4.store_id = 0", + self::LINK_FIELD, + self::LINK_FIELD, + self::ATTR_ID + ) + ] + )->willReturn($select); + + $expressionMock = $this->createMock(Expression::class); + $this->expressionFactory->expects($this->exactly(4)) + ->method('create') + ->willReturnMap($this->getExpressionsMap($expressionMock)); + $select->expects($this->exactly(3))->method('columns') + ->withConsecutive( + [ + ['attribute_id'] + ], + [ + ['attribute_id'] + ], + [ + [ + 'STORE_CODE' => 's.code', + 'ACTIVE_IN_MENU' => $expressionMock, + 'ACTIVE_NOT_IN_MENU' => $expressionMock, + 'NOT_ACTIVE_IN_MENU' => $expressionMock, + 'NOT_ACTIVE_NOT_IN_MENU' => $expressionMock + ] + ] + )->willReturn($select); + + $select->expects($this->once())->method('group')->with('s.code'); + + return $select; + } + + private function getExpressionsMap(Expression $expressionMock): array + { + $expression = 'COUNT(( + %s IF(ccei1.value IS NULL, ccei2.value, ccei1.value) AND + %s IF(ccei3.value IS NULL, ccei4.value, ccei3.value) + ) or null)'; + + return [ + [['expression' => sprintf($expression, '', '')], $expressionMock], + [['expression' => sprintf($expression, '', 'NOT')], $expressionMock], + [['expression' => sprintf($expression, 'NOT', '')], $expressionMock], + [['expression' => sprintf($expression, 'NOT', 'NOT')], $expressionMock], + ]; + } + + private function getTableNamesMap(): array + { + return [ + ['eav_attribute', self::T_ATT], + ['catalog_category_entity_int', self::T_CAT_ENT_INT], + ['catalog_category_entity', self::T_CAT_ENT], + ['store', self::T_STORE], + ['store_group', self::T_STORE_GROUP], + ]; + } + + public function testAggregate(): void + { + $connection = $this->createMock(AdapterInterface::class); + $statisticData = $this->getStatisticData(); + $this->resourceConnection->expects($this->once())->method('getConnection')->willReturn($connection); + $connection->expects($this->exactly(10)) + ->method('getTableName') + ->willReturnMap($this->getTableNamesMap()); + $entityMetadata = $this->createMock(EntityMetadataInterface::class); + $entityMetadata->expects($this->once())->method('getLinkField')->willReturn(self::LINK_FIELD); + $this->metadataPool->expects($this->once())->method('getMetadata') + ->with(CategoryInterface::class)->willReturn($entityMetadata); + + $select = $this->getSelectMock(); + $connection->expects($this->exactly(3))->method('select')->willReturn($select); + $connection->expects($this->exactly(2))->method('fetchOne')->willReturn(self::ATTR_ID); + $connection->expects($this->once()) + ->method('fetchAll') + ->with($select) + ->willReturn($statisticData); + + $this->updateMetricService->expects($this->exactly(4 * count($statisticData))) + ->method('update') + ->withConsecutive(...$this->getUpdateMetricsArguments($statisticData)); + + $this->subject->aggregate(); + } + + private function getUpdateMetricsArguments(array $statisticData): array + { + $arguments = []; + + foreach ($statisticData as $datum) { + $label = ['store_code' => $datum['STORE_CODE']]; + $arguments[] = [ + self::METRIC_CODE, + $datum['ACTIVE_IN_MENU'], + array_merge(['status' => 'enabled', 'menu_status' => 'enabled'], $label) + ]; + $arguments[] = [ + self::METRIC_CODE, + $datum['ACTIVE_NOT_IN_MENU'], + array_merge(['status' => 'enabled', 'menu_status' => 'disabled'], $label) + ]; + $arguments[] = [ + self::METRIC_CODE, + $datum['NOT_ACTIVE_IN_MENU'], + array_merge(['status' => 'disabled', 'menu_status' => 'enabled'], $label) + ]; + $arguments[] = [ + self::METRIC_CODE, + $datum['NOT_ACTIVE_NOT_IN_MENU'], + array_merge(['status' => 'disabled', 'menu_status' => 'disabled'], $label) + ]; + } + + return $arguments; + } +} diff --git a/src/Aggregator/Category/CategoryCountAggregator.php b/src/Aggregator/Category/CategoryCountAggregator.php new file mode 100644 index 0000000..ac9fe8b --- /dev/null +++ b/src/Aggregator/Category/CategoryCountAggregator.php @@ -0,0 +1,197 @@ +updateMetricService = $updateMetricService; + $this->resourceConnection = $resourceConnection; + $this->metadataPool = $metadataPool; + $this->expressionFactory = $expressionFactory; + } + + public function getCode(): string + { + return self::METRIC_CODE; + } + + public function getHelp(): string + { + return 'Magento 2 Categories count by status and store code.'; + } + + public function getType(): string + { + return 'gauge'; + } + + public function aggregate(): bool + { + $connection = $this->resourceConnection->getConnection(); + + $data = $connection->fetchAll($this->getSelect($connection)); + + foreach ($data as $datum) { + $labels = ['store_code' => $datum['STORE_CODE']]; + + $this->updateMetrics( + (string)$datum['ACTIVE_IN_MENU'], + array_merge($labels, ['status' => 'enabled', 'menu_status' => 'enabled']) + ); + $this->updateMetrics( + (string)$datum['ACTIVE_NOT_IN_MENU'], + array_merge($labels, ['status' => 'enabled', 'menu_status' => 'disabled']) + ); + $this->updateMetrics( + (string)$datum['NOT_ACTIVE_IN_MENU'], + array_merge($labels, ['status' => 'disabled', 'menu_status' => 'enabled']) + ); + $this->updateMetrics( + (string)$datum['NOT_ACTIVE_NOT_IN_MENU'], + array_merge($labels, ['status' => 'disabled', 'menu_status' => 'disabled']) + ); + } + + return true; + } + + private function updateMetrics(string $count, array $labels): void + { + $this->updateMetricService->update(self::METRIC_CODE, $count, $labels); + } + + /** + * SQL example: + * select s.code, + * COUNT( (IF (ccei1.value IS NULL, ccei2.value, ccei1.value) AND IF (ccei3.value IS NULL, ccei4.value, + * ccei3.value)) or null) as active_in_menu, COUNT( (IF (ccei1.value IS NULL, ccei2.value, ccei1.value) AND NOT IF + * (ccei3.value IS NULL, ccei4.value, ccei3.value)) or null) as active_not_in_menu, COUNT( (NOT IF (ccei1.value IS + * NULL, ccei2.value, ccei1.value) AND IF (ccei3.value IS NULL, ccei4.value, ccei3.value)) or null) as + * disabled_in_menu, COUNT( (NOT IF (ccei1.value IS NULL, ccei2.value, ccei1.value) AND NOT IF (ccei3.value IS + * NULL, ccei4.value, ccei3.value)) or null) as disabled_not_in_menu from store_group sg inner join store s on + * s.group_id = sg.group_id inner join catalog_category_entity cce1 on sg.root_category_id = cce1.entity_id inner + * join catalog_category_entity cce2 on cce2.path like CONCAT(cce1.path, '%') left join catalog_category_entity_int + * ccei1 on ccei1.entity_id = cce2.entity_id and ccei1.attribute_id = 32 and ccei1.store_id = s.store_id left join + * catalog_category_entity_int ccei2 on ccei2.entity_id = cce2.entity_id and ccei2.attribute_id = 32 and + * ccei2.store_id = 0 left join catalog_category_entity_int ccei3 on ccei3.entity_id = cce2.entity_id and ccei3.attribute_id = 601 and ccei3.store_id = s.store_id left join catalog_category_entity_int ccei4 on ccei4.entity_id = cce2.entity_id and ccei4.attribute_id = 601 and ccei4.store_id = 0 group by s.code; + * + * + * @param AdapterInterface $connection + * + * @return Select + * @throws \Exception + */ + private function getSelect(AdapterInterface $connection): Select + { + $linkField = $this->metadataPool->getMetadata(CategoryInterface::class)->getLinkField(); + $isActive = $this->getIsActiveAttributeId($connection); + $isInMenu = $this->getIsInMenuAttributeId($connection); + $expression = 'COUNT(( + %s IF(ccei1.value IS NULL, ccei2.value, ccei1.value) AND + %s IF(ccei3.value IS NULL, ccei4.value, ccei3.value) + ) or null)'; + + $activeInMenu = $this->expressionFactory->create(['expression' => sprintf($expression, '', '')]); + $activeNotInMenu = $this->expressionFactory->create(['expression' => sprintf($expression, '', 'NOT')]); + $notActiveInMenu = $this->expressionFactory->create(['expression' => sprintf($expression, 'NOT', '')]); + $notActiveNotInMenu = $this->expressionFactory->create( + ['expression' => sprintf($expression, 'NOT', 'NOT')] + ); + + $select = $connection->select(); + + $select->from(['sg' => $connection->getTableName('store_group')]) + ->joinInner( + ['s' => $connection->getTableName('store')], + 'sg.group_id = s.group_id' + )->joinInner( + ['cce1' => $connection->getTableName('catalog_category_entity')], + 'sg.root_category_id = cce1.entity_id' + )->joinInner( + ['cce2' => $connection->getTableName('catalog_category_entity')], + "cce2.path like CONCAT(cce1.path, '%')" + )->joinLeft( + ['ccei1' => $connection->getTableName('catalog_category_entity_int')], + "cce2.$linkField = ccei1.$linkField AND " . + "ccei1.attribute_id = $isActive AND ccei1.store_id = s.store_id" + )->joinLeft( + ['ccei2' => $connection->getTableName('catalog_category_entity_int')], + "cce2.$linkField = ccei2.$linkField AND " . + "ccei2.attribute_id = $isActive AND ccei2.store_id = 0" + )->joinLeft( + ['ccei3' => $connection->getTableName('catalog_category_entity_int')], + "cce2.$linkField = ccei3.$linkField AND " . + "ccei3.attribute_id = $isInMenu AND ccei3.store_id = s.store_id" + )->joinLeft( + ['ccei4' => $connection->getTableName('catalog_category_entity_int')], + "cce2.$linkField = ccei4.$linkField AND " . + "ccei4.attribute_id = $isInMenu AND ccei4.store_id = 0" + )->reset(Select::COLUMNS) + ->columns( + [ + 'STORE_CODE' => 's.code', + 'ACTIVE_IN_MENU' => $activeInMenu, + 'ACTIVE_NOT_IN_MENU' => $activeNotInMenu, + 'NOT_ACTIVE_IN_MENU' => $notActiveInMenu, + 'NOT_ACTIVE_NOT_IN_MENU' => $notActiveNotInMenu + ] + )->group('s.code'); + + return $select; + } + + private function getIsActiveAttributeId(AdapterInterface $connection): int + { + return $this->getAttributeId($connection, 'is_active'); + } + + private function getIsInMenuAttributeId(AdapterInterface $connection): int + { + return $this->getAttributeId($connection, 'include_in_menu'); + } + + private function getAttributeId(AdapterInterface $connection, string $code): int + { + $select = $connection->select(); + + $select->from($connection->getTableName('eav_attribute')) + ->where('entity_type_id = ?', self::CATEGORY_ENTITY_ID) + ->where('attribute_code = ?', $code) + ->reset(Select::COLUMNS) + ->columns(['attribute_id']); + + return (int)$connection->fetchOne($select); + } +} diff --git a/src/Aggregator/Shipment/ShipmentCountAggregator.php b/src/Aggregator/Shipment/ShipmentCountAggregator.php index 1730cc8..bf13024 100644 --- a/src/Aggregator/Shipment/ShipmentCountAggregator.php +++ b/src/Aggregator/Shipment/ShipmentCountAggregator.php @@ -35,7 +35,7 @@ public function getCode(): string public function getHelp(): string { - return 'Magento 2 Shipments amount by store and source.'; + return 'Magento 2 Shipments count by store and source.'; } public function getType(): string diff --git a/src/etc/di.xml b/src/etc/di.xml index a732b48..79af7f5 100644 --- a/src/etc/di.xml +++ b/src/etc/di.xml @@ -8,6 +8,9 @@ + + RunAsRoot\PrometheusExporter\Aggregator\Category\CategoryCountAggregator + RunAsRoot\PrometheusExporter\Aggregator\Cms\CmsBlockCountAggregator RunAsRoot\PrometheusExporter\Aggregator\Cms\CmsPagesCountAggregator