autowp/autowp

View on GitHub
module/Application/src/Model/ItemParent.php

Summary

Maintainability
F
5 days
Test Coverage
D
62%
<?php

namespace Application\Model;

use ArrayAccess;
use ArrayObject;
use Autowp\ZFComponents\Filter\FilenameSafe;
use Exception;
use Laminas\Db\Adapter\Adapter;
use Laminas\Db\Sql;
use Laminas\Db\TableGateway\TableGateway;

use function array_key_exists;
use function array_keys;
use function array_merge;
use function array_replace;
use function array_unique;
use function Autowp\Commons\currentFromResultSetInterface;
use function count;
use function in_array;
use function ltrim;
use function mb_strlen;
use function mb_substr;
use function preg_quote;
use function preg_replace;
use function sprintf;
use function str_ireplace;
use function trim;

class ItemParent
{
    public const MAX_CATNAME       = 150;
    public const MAX_LANGUAGE_NAME = 255;

    public const
        TYPE_DEFAULT = 0,
        TYPE_TUNING  = 1,
        TYPE_SPORT   = 2,
        TYPE_DESIGN  = 3;

    private TableGateway $itemTable;

    private TableGateway $itemParentTable;

    private TableGateway $itemParentLanguageTable;

    private array $languages;

    private array $allowedCombinations = [
        Item::VEHICLE   => [
            Item::VEHICLE => true,
        ],
        Item::ENGINE    => [
            Item::ENGINE => true,
        ],
        Item::CATEGORY  => [
            Item::VEHICLE  => true,
            Item::CATEGORY => true,
            Item::BRAND    => true,
        ],
        Item::TWINS     => [
            Item::VEHICLE => true,
        ],
        Item::BRAND     => [
            Item::BRAND   => true,
            Item::VEHICLE => true,
            Item::ENGINE  => true,
        ],
        Item::FACTORY   => [
            Item::VEHICLE => true,
            Item::ENGINE  => true,
        ],
        Item::PERSON    => [],
        Item::COPYRIGHT => [],
    ];

    private array $catnameBlacklist = ['sport', 'tuning', 'related', 'pictures', 'specifications'];

    private TableGateway $specTable;

    private TableGateway $itemParentCacheTable;

    private ItemAlias $itemAlias;

    private Item $itemModel;

    public function __construct(
        array $languages,
        TableGateway $specTable,
        TableGateway $itemParentTable,
        TableGateway $itemTable,
        TableGateway $itemParentLanguageTable,
        TableGateway $itemParentCacheTable,
        ItemAlias $itemAlias,
        Item $itemModel
    ) {
        $this->languages = $languages;
        $this->specTable = $specTable;
        $this->itemModel = $itemModel;

        $this->itemTable               = $itemTable;
        $this->itemParentTable         = $itemParentTable;
        $this->itemParentLanguageTable = $itemParentLanguageTable;
        $this->itemParentCacheTable    = $itemParentCacheTable;
        $this->itemAlias               = $itemAlias;
    }

    /**
     * @throws Exception
     */
    public function delete(int $parentId, int $itemId): bool
    {
        $brandRow = currentFromResultSetInterface($this->itemTable->select(['id' => $parentId]));
        if (! $brandRow) {
            return false;
        }

        $this->itemParentLanguageTable->delete([
            'parent_id' => $parentId,
            'item_id'   => $itemId,
        ]);

        $this->itemParentTable->delete([
            'parent_id' => $parentId,
            'item_id'   => $itemId,
        ]);

        return true;
    }

    private function getYearsPrefix(?int $begin, ?int $end): string
    {
        if (! $begin && ! $end) {
            return '';
        }

        $bms = (int) ($begin / 100);
        $ems = (int) ($end / 100);

        if ($end === $begin) {
            return (string) $begin;
        }

        if ($bms === $ems) {
            return $begin . '–' . sprintf('%02d', $end % 100);
        }

        if (! $begin) {
            return 'xx–' . $end;
        }

        if ($end) {
            return $begin . '–' . $end;
        }

        return $begin . '–xx';
    }

    /**
     * @param array|ArrayAccess $parentRow
     * @param array|ArrayAccess $vehicleRow
     * @throws Exception
     */
    private function extractName($parentRow, $vehicleRow, string $language): string
    {
        $langName = $this->itemModel->getName($vehicleRow['id'], $language);

        $vehicleName = $langName ?: $vehicleRow['name'];

        $aliases = $this->itemAlias->getAliases($parentRow['id']);

        $name = $vehicleName;
        foreach ($aliases as $alias) {
            $name = str_ireplace('by The ' . $alias . ' Company', '', $name);
            $name = str_ireplace('by ' . $alias, '', $name);
            $name = str_ireplace('di ' . $alias, '', $name);
            $name = str_ireplace('par ' . $alias, '', $name);
            $name = str_ireplace($alias . '-', '', $name);
            $name = str_ireplace('-' . $alias, '', $name);

            $name = preg_replace('/\b' . preg_quote($alias, '/') . '\b/iu', '', $name);
        }

        $name = trim(preg_replace("|[[:space:]]+|", ' ', $name));
        $name = ltrim($name, '/');
        if (! $name) {
            if ($vehicleRow['body'] && ($vehicleRow['body'] !== $parentRow['body'])) {
                $name = $vehicleRow['body'];
            }
        }

        // TODO: fractions
        if (! $name && $vehicleRow['begin_model_year']) {
            $modelYearsDifferent = $vehicleRow['begin_model_year'] !== $parentRow['begin_model_year']
                || $vehicleRow['end_model_year'] !== $parentRow['end_model_year'];
            if ($modelYearsDifferent) {
                $name = $this->getYearsPrefix($vehicleRow['begin_model_year'], $vehicleRow['end_model_year']);
            }
        }

        if (! $name && $vehicleRow['begin_year']) {
            $yearsDifferent = $vehicleRow['begin_year'] !== $parentRow['begin_year']
                || $vehicleRow['end_year'] !== $parentRow['end_year'];
            if ($yearsDifferent) {
                $name = $this->getYearsPrefix($vehicleRow['begin_year'], $vehicleRow['end_year']);
            }
        }

        if (! $name && $vehicleRow['spec_id']) {
            $specsDifferent = $vehicleRow['spec_id'] !== $parentRow['spec_id'];
            if ($specsDifferent) {
                $specRow = currentFromResultSetInterface(
                    $this->specTable->select(['id' => (int) $vehicleRow['spec_id']])
                );

                if ($specRow) {
                    $name = $specRow['short_name'];
                }
            }
        }

        if (! $name) {
            $name = $vehicleName;
        }

        return $name;
    }

    /**
     * @throws Exception
     */
    private function isAllowedCatname(int $itemId, int $parentId, string $catname): bool
    {
        if (mb_strlen($catname) <= 0) {
            return false;
        }

        if (in_array($catname, $this->catnameBlacklist)) {
            return false;
        }

        return ! currentFromResultSetInterface($this->itemParentTable->select([
            'parent_id'    => $parentId,
            'catname'      => $catname,
            'item_id != ?' => $itemId,
        ]));
    }

    /**
     * @param array|ArrayAccess $brandRow
     * @param array|ArrayAccess $vehicleRow
     * @throws Exception
     */
    private function extractCatname($brandRow, $vehicleRow): string
    {
        $diffName = $this->getNamePreferLanguage($brandRow['id'], $vehicleRow['id'], 'en');
        if (! $diffName) {
            $diffName = $this->extractName($brandRow, $vehicleRow, 'en');
        }

        $filter          = new FilenameSafe();
        $catnameTemplate = $filter->filter($diffName);

        $i = 0;
        do {
            $catname = $catnameTemplate . ($i ? '_' . $i : '');

            $allowed = $this->isAllowedCatname($vehicleRow['id'], $brandRow['id'], $catname);

            $i++;
        } while (! $allowed);

        return $catname;
    }

    public function isAllowedCombination(int $itemTypeId, int $parentItemTypeId): bool
    {
        return isset($this->allowedCombinations[$parentItemTypeId][$itemTypeId]);
    }

    /**
     * @throws Exception
     */
    public function create(int $parentId, int $itemId, array $options = []): bool
    {
        $parentRow = currentFromResultSetInterface($this->itemTable->select(['id' => $parentId]));
        $itemRow   = currentFromResultSetInterface($this->itemTable->select(['id' => $itemId]));
        if (! $parentRow || ! $itemRow) {
            return false;
        }

        if (! $parentRow['is_group']) {
            throw new Exception("Only groups can have childs");
        }

        if (! $this->isAllowedCombination($itemRow['item_type_id'], $parentRow['item_type_id'])) {
            throw new Exception("That type of parent is not allowed for this type");
        }

        $itemId   = (int) $itemRow['id'];
        $parentId = (int) $parentRow['id'];

        if (isset($options['catname'])) {
            $allowed = $this->isAllowedCatname($itemId, $parentId, $options['catname']);
            if (! $allowed) {
                unset($options['catname']);
            }
        }

        if (array_key_exists('type', $options) && $options['type'] === null) {
            unset($options['type']);
        }

        $defaults = [
            'type'           => self::TYPE_DEFAULT,
            'catname'        => null,
            'manual_catname' => isset($options['catname']),
        ];
        $options  = array_replace($defaults, $options);

        if (! isset($options['catname']) || ! $options['catname'] || $options['catname'] === '_') {
            $catname = $this->extractCatname($parentRow, $itemRow);
            if (! $catname) {
                throw new Exception('Failed to create catname');
            }

            $options['catname'] = $catname;
        }

        if (! isset($options['type'])) {
            throw new Exception("Type cannot be null");
        }

        $parentIds = $this->collectAncestorsIds($parentId);
        if (in_array($itemId, $parentIds)) {
            throw new Exception('Cycle detected');
        }

        $itemParentRow = currentFromResultSetInterface($this->itemParentTable->select([
            'parent_id' => $parentId,
            'item_id'   => $itemId,
        ]));

        if ($itemParentRow) {
            return false;
        }

        $this->itemParentTable->insert([
            'parent_id'      => $parentId,
            'item_id'        => $itemId,
            'type'           => $options['type'],
            'catname'        => $options['catname'],
            'manual_catname' => $options['manual_catname'] ? 1 : 0,
            'timestamp'      => new Sql\Expression('now()'),
        ]);

        $values = [];
        foreach ($this->languages as $language) {
            $values[$language] = [
                'name' => $this->extractName($parentRow, $itemRow, $language),
            ];
        }

        $this->setItemParentLanguages($parentId, $itemId, $values, true);

        $this->rebuildCache($itemId);

        return true;
    }

    /**
     * @throws Exception
     */
    public function move(int $itemId, int $fromParentId, int $toParentId): bool
    {
        $oldParentRow = currentFromResultSetInterface($this->itemTable->select(['id' => $fromParentId]));
        $newParentRow = currentFromResultSetInterface($this->itemTable->select(['id' => $toParentId]));
        $itemRow      = currentFromResultSetInterface($this->itemTable->select(['id' => $itemId]));
        if (! $oldParentRow || ! $newParentRow || ! $itemRow) {
            return false;
        }

        if ((int) $oldParentRow['id'] === (int) $newParentRow['id']) {
            return false;
        }

        if (! $oldParentRow['is_group']) {
            throw new Exception("Only groups can have childs");
        }

        if (! $newParentRow['is_group']) {
            throw new Exception("Only groups can have childs");
        }

        if (! $this->isAllowedCombination($itemRow['item_type_id'], $newParentRow['item_type_id'])) {
            throw new Exception("That type of parent is not allowed for this type");
        }

        $itemId = (int) $itemRow['id'];

        $parentIds = $this->collectAncestorsIds($newParentRow['id']);
        if (in_array($itemId, $parentIds)) {
            throw new Exception('Cycle detected');
        }

        $primaryKey = [
            'parent_id' => $fromParentId,
            'item_id'   => $itemId,
        ];

        $itemParentRow = currentFromResultSetInterface($this->itemParentTable->select($primaryKey));

        if (! $itemParentRow) {
            return false;
        }

        $this->itemParentTable->update([
            'parent_id' => $toParentId,
        ], $primaryKey);

        $this->itemParentLanguageTable->update([
            'parent_id' => $toParentId,
        ], $primaryKey);

        $this->rebuildCache($itemRow['id']);

        $this->refreshAuto($toParentId, $itemId);

        return true;
    }

    /**
     * @throws Exception
     */
    public function remove(int $parentId, int $itemId): void
    {
        $parentRow = currentFromResultSetInterface($this->itemTable->select(['id' => $parentId]));
        $itemRow   = currentFromResultSetInterface($this->itemTable->select(['id' => $itemId]));
        if (! $parentRow || ! $itemRow) {
            return;
        }

        $itemId   = (int) $itemRow['id'];
        $parentId = (int) $parentRow['id'];

        $this->itemParentTable->delete([
            'item_id = ?'   => $itemId,
            'parent_id = ?' => $parentId,
        ]);

        $this->itemParentLanguageTable->delete([
            'item_id = ?'   => $itemId,
            'parent_id = ?' => $parentId,
        ]);

        $this->rebuildCache($itemRow['id']);
    }

    /**
     * @throws Exception
     */
    public function setItemParentLanguage(
        int $parentId,
        int $itemId,
        string $language,
        array $values,
        bool $forceIsAuto
    ): void {
        $primaryKey = [
            'item_id'   => $itemId,
            'parent_id' => $parentId,
            'language'  => $language,
        ];

        $bvlRow = currentFromResultSetInterface($this->itemParentLanguageTable->select($primaryKey));

        if ($forceIsAuto) {
            $isAuto = true;
        } else {
            $isAuto = $bvlRow ? $bvlRow['is_auto'] : true;
            $name   = $bvlRow ? $bvlRow['name'] : '';
            if (! array_key_exists('name', $values)) {
                throw new Exception("`name` not provided");
            }
            if ($name !== $values['name']) {
                $isAuto = false;
            }
        }

        if (! $values['name']) {
            $parentRow      = currentFromResultSetInterface($this->itemTable->select(['id' => $parentId]));
            $itemRow        = currentFromResultSetInterface($this->itemTable->select(['id' => $itemId]));
            $values['name'] = $this->extractName($parentRow, $itemRow, $language);
            $isAuto         = true;
        }

        $params = array_replace([
            'name'    => mb_substr($values['name'], 0, self::MAX_LANGUAGE_NAME),
            'is_auto' => $isAuto ? 1 : 0,
        ], $primaryKey);
        /** @var Adapter $adapter */
        $adapter = $this->itemParentLanguageTable->getAdapter();
        $adapter->query('
            INSERT INTO item_parent_language (item_id, parent_id, language, name, is_auto)
            VALUES (:item_id, :parent_id, :language, :name, :is_auto)
            ON DUPLICATE KEY UPDATE name = VALUES(name), is_auto = VALUES(is_auto)
        ', $params);
    }

    /**
     * @throws Exception
     */
    private function setItemParentLanguages(int $parentId, int $itemId, array $values, bool $forceIsAuto): void
    {
        foreach ($this->languages as $language) {
            $languageValues = [
                'name' => null,
            ];
            if (isset($values[$language])) {
                $languageValues = $values[$language];
            }
            $this->setItemParentLanguage($parentId, $itemId, $language, $languageValues, $forceIsAuto);
        }
    }

    /**
     * @throws Exception
     */
    public function setItemParent(int $parentId, int $itemId, array $values, bool $forceIsAuto): bool
    {
        $itemParentRow = currentFromResultSetInterface($this->itemParentTable->select([
            'parent_id' => $parentId,
            'item_id'   => $itemId,
        ]));

        if (! $itemParentRow) {
            return false;
        }

        $set          = [];
        $rebuildCache = false;

        if (array_key_exists('type', $values)) {
            $set['type']  = $values['type'];
            $rebuildCache = true;
        }

        if (array_key_exists('catname', $values)) {
            $newCatname = $values['catname'];

            if ($forceIsAuto) {
                $isAuto = true;
            } else {
                $isAuto = ! $itemParentRow['manual_catname'];
                if ($itemParentRow['catname'] !== $newCatname) {
                    $isAuto = false;
                }
            }

            if (! $newCatname || $newCatname === '_' || in_array($newCatname, $this->catnameBlacklist)) {
                $parentRow  = currentFromResultSetInterface($this->itemTable->select(['id' => $parentId]));
                $itemRow    = currentFromResultSetInterface($this->itemTable->select(['id' => $itemId]));
                $newCatname = $this->extractCatname($parentRow, $itemRow);
                $isAuto     = true;
            }

            $set = array_replace($set, [
                'catname'        => $newCatname,
                'manual_catname' => $isAuto ? 0 : 1,
            ]);
        }

        if ($set) {
            $this->itemParentTable->update($set, [
                'parent_id = ?' => $parentId,
                'item_id = ?'   => $itemId,
            ]);
        }

        if ($rebuildCache) {
            $this->rebuildCache($itemId);
        }

        return true;
    }

    /**
     * @throws Exception
     */
    public function refreshAuto(int $parentId, int $itemId): bool
    {
        $bvlRows = $this->itemParentLanguageTable->select([
            'item_id'   => $itemId,
            'parent_id' => $parentId,
        ]);

        $values = [];
        foreach ($bvlRows as $bvlRow) {
            $values[$bvlRow['language']] = [
                'name' => $bvlRow['is_auto'] ? null : $bvlRow['name'],
            ];
        }

        $this->setItemParentLanguages($parentId, $itemId, $values, false);

        $bvRow = currentFromResultSetInterface($this->itemParentTable->select([
            'item_id'   => $itemId,
            'parent_id' => $parentId,
        ]));

        if (! $bvRow) {
            return false;
        }
        if (! $bvRow['manual_catname']) {
            $brandRow   = currentFromResultSetInterface($this->itemTable->select(['id' => $parentId]));
            $vehicleRow = currentFromResultSetInterface($this->itemTable->select(['id' => $itemId]));

            $catname = $this->extractCatname($brandRow, $vehicleRow);
            if (! $catname) {
                return false;
            }

            $this->itemParentTable->update([
                'catname' => $catname,
            ], [
                'item_id = ?'   => $itemId,
                'parent_id = ?' => $parentId,
            ]);
        }

        return true;
    }

    /**
     * @throws Exception
     */
    public function refreshAutoByVehicle(int $itemId): bool
    {
        foreach ($this->getParentRows($itemId) as $itemParentRow) {
            $this->refreshAuto($itemParentRow['parent_id'], $itemId);
        }

        return true;
    }

    /**
     * @throws Exception
     */
    public function refreshAllAuto(): bool
    {
        $itemParentRows = $this->itemParentTable->select([
            'not manual_catname',
        ]);

        foreach ($itemParentRows as $itemParentRow) {
            $this->refreshAuto($itemParentRow['parent_id'], $itemParentRow['item_id']);
        }

        return true;
    }

    public function getParentRows(int $itemId, bool $stockFirst = false): array
    {
        $select = new Sql\Select($this->itemParentTable->getTable());
        $select->where(['item_id' => $itemId]);

        if ($stockFirst) {
            $select->order([
                new Sql\Expression('type = ? desc', [self::TYPE_DEFAULT]),
            ]);
        }

        $rows = $this->itemParentTable->selectWith($select);

        $result = [];
        foreach ($rows as $row) {
            $result[] = $row;
        }

        return $result;
    }

    public function getParentIds(int $itemId): array
    {
        $select = new Sql\Select($this->itemParentTable->getTable());
        $select->columns(['parent_id'])
            ->where(['item_id' => $itemId]);

        $rows = $this->itemParentTable->selectWith($select);

        $result = [];
        foreach ($rows as $row) {
            $result[] = (int) $row['parent_id'];
        }

        return $result;
    }

    /**
     * @return array|ArrayObject|null
     * @throws Exception
     */
    public function getRow(int $parentId, int $itemId)
    {
        return currentFromResultSetInterface($this->itemParentTable->select([
            'parent_id' => $parentId,
            'item_id'   => $itemId,
        ]));
    }

    /**
     * @return array|ArrayObject|null
     * @throws Exception
     */
    public function getRowByCatname(int $parentId, string $catname)
    {
        return currentFromResultSetInterface($this->itemParentTable->select([
            'parent_id' => $parentId,
            'catname'   => $catname,
        ]));
    }

    /**
     * @throws Exception
     */
    public function getName(int $parentId, int $itemId, string $language): ?string
    {
        $bvlRow = currentFromResultSetInterface($this->itemParentLanguageTable->select([
            'parent_id' => $parentId,
            'item_id'   => $itemId,
            'language'  => $language,
        ]));

        if (! $bvlRow) {
            return null;
        }

        return $bvlRow['name'];
    }

    /**
     * @throws Exception
     */
    public function getNamePreferLanguage(int $parentId, int $itemId, string $language): string
    {
        $select = new Sql\Select($this->itemParentLanguageTable->getTable());
        $select->columns(['name'])
            ->where([
                'item_id = ?'   => $itemId,
                'parent_id = ?' => $parentId,
                'length(name) > 0',
            ])
            ->order(new Sql\Expression('language = ? desc', [$language]));

        $row = currentFromResultSetInterface($this->itemParentLanguageTable->selectWith($select));

        return $row ? $row['name'] : '';
    }

    public function getChildItemsIds(int $parentId): array
    {
        $select = new Sql\Select($this->itemParentTable->getTable());
        $select->columns(['item_id'])
            ->where(['parent_id' => $parentId]);

        $result = [];
        foreach ($this->itemParentTable->selectWith($select) as $row) {
            $result[] = (int) $row['item_id'];
        }

        return $result;
    }

    private function collectAncestorsIds(int $id): array
    {
        $cpTableName = $this->itemParentTable->getTable();

        $toCheck = [$id];
        $ids     = [];

        while (count($toCheck) > 0) {
            $ids = array_merge($ids, $toCheck);

            $select = new Sql\Select($cpTableName);
            $select->columns(['parent_id'])
                ->where([new Sql\Predicate\In('item_id', $toCheck)]);

            $toCheck = [];
            foreach ($this->itemParentTable->selectWith($select) as $row) {
                $toCheck [] = (int) $row['parent_id'];
            }
        }

        return array_unique($ids);
    }

    private function collectParentInfo(int $id, int $diff = 1): array
    {
        $select = new Sql\Select($this->itemParentTable->getTable());
        $select->columns(['parent_id', 'type'])
            ->where(['item_id' => $id]);

        $rows = $this->itemParentTable->selectWith($select);

        $result = [];
        foreach ($rows as $row) {
            $parentId          = $row['parent_id'];
            $isTuning          = (int) $row['type'] === self::TYPE_TUNING;
            $isSport           = (int) $row['type'] === self::TYPE_SPORT;
            $isDesign          = (int) $row['type'] === self::TYPE_DESIGN;
            $result[$parentId] = [
                'diff'   => $diff,
                'tuning' => $isTuning,
                'sport'  => $isSport,
                'design' => $isDesign,
            ];

            foreach ($this->collectParentInfo($parentId, $diff + 1) as $pid => $info) {
                if (! isset($result[$pid]) || $info['diff'] < $result[$pid]['diff']) {
                    $result[$pid]           = $info;
                    $result[$pid]['tuning'] = $result[$pid]['tuning'] || $isTuning;
                    $result[$pid]['sport']  = $result[$pid]['sport'] || $isSport;
                    $result[$pid]['design'] = $result[$pid]['design'] || $isDesign;
                }
            }
        }

        return $result;
    }

    public function rebuildCache(int $itemId): int
    {
        $parentInfo          = $this->collectParentInfo($itemId);
        $parentInfo[$itemId] = [
            'diff'   => 0,
            'tuning' => false,
            'sport'  => false,
            'design' => false,
        ];

        $updates = 0;

        /** @var Adapter $adapter */
        $adapter = $this->itemParentCacheTable->getAdapter();
        $stmt    = $adapter->createStatement('
            INSERT INTO item_parent_cache (item_id, parent_id, diff, tuning, sport, design)
            VALUES (:item_id, :parent_id, :diff, :tuning, :sport, :design)
            ON DUPLICATE KEY UPDATE
                diff = VALUES(diff),
                tuning = VALUES(tuning),
                sport = VALUES(sport),
                design = VALUES(design)
        ');

        foreach ($parentInfo as $parentId => $info) {
            $result   = $stmt->execute([
                'item_id'   => $itemId,
                'parent_id' => $parentId,
                'diff'      => $info['diff'],
                'tuning'    => $info['tuning'] ? 1 : 0,
                'sport'     => $info['sport'] ? 1 : 0,
                'design'    => $info['design'] ? 1 : 0,
            ]);
            $updates += $result->getAffectedRows();
        }

        $filter = [
            'item_id = ?' => $itemId,
            new Sql\Predicate\NotIn('parent_id', array_keys($parentInfo)),
        ];

        $updates += $this->itemParentCacheTable->delete($filter);

        $childs = $this->getChildItemsIds($itemId);

        foreach ($childs as $child) {
            $updates += $this->rebuildCache($child);
        }

        return $updates;
    }

    /**
     * @throws Exception
     */
    public function hasChildItems(int $parentId): bool
    {
        $select = new Sql\Select($this->itemParentTable->getTable());
        $select->columns(['item_id'])
            ->where(['parent_id' => $parentId])
            ->limit(1);

        return (bool) currentFromResultSetInterface($this->itemParentTable->selectWith($select));
    }

    /**
     * @throws Exception
     */
    public function getChildItemsCount(int $parentId): int
    {
        $select = new Sql\Select($this->itemParentTable->getTable());
        $select->columns(['count' => new Sql\Expression('count(1)')])
            ->where(['parent_id' => $parentId]);

        $row = currentFromResultSetInterface($this->itemParentTable->selectWith($select));
        return $row ? (int) $row['count'] : 0;
    }

    /**
     * @throws Exception
     */
    public function getParentItemsCount(int $itemId): int
    {
        $select = new Sql\Select($this->itemParentTable->getTable());
        $select->columns(['count' => new Sql\Expression('count(1)')])
            ->where(['item_id' => $itemId]);

        $row = currentFromResultSetInterface($this->itemParentTable->selectWith($select));
        return $row ? (int) $row['count'] : 0;
    }

    public function getChildItemsCountArray(array $parentIds): array
    {
        if (count($parentIds) <= 0) {
            return [];
        }

        $select = new Sql\Select($this->itemParentTable->getTable());
        $select->columns(['parent_id', 'count' => new Sql\Expression('count(1)')])
            ->where([new Sql\Predicate\In('parent_id', $parentIds)])
            ->group('parent_id');

        $result = [];
        foreach ($this->itemParentTable->selectWith($select) as $row) {
            $result[(int) $row['parent_id']] = (int) $row['count'];
        }

        return $result;
    }

    public function getTable(): TableGateway
    {
        return $this->itemParentTable;
    }

    public function getChildItemLinkTypesCount(int $itemId): array
    {
        $select = new Sql\Select($this->itemParentTable->getTable());

        $select->columns(['type', 'count' => new Sql\Expression('count(1)')])
            ->where(['parent_id' => $itemId])
            ->group('type');

        $result = [];
        foreach ($this->itemParentTable->selectWith($select) as $row) {
            $typeId          = (int) $row['type'];
            $result[$typeId] = (int) $row['count'];
        }

        return $result;
    }
}