* @copyright Since 2007 PrestaShop SA and Contributors * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) */ declare(strict_types=1); namespace PrestaShop\PrestaShop\Core\Repository; use function bqSQL; use Db; use DbQuery; use ObjectModel; use PrestaShop\PrestaShop\Core\Domain\Shop\Exception\ShopAssociationNotFound; use PrestaShop\PrestaShop\Core\Domain\Shop\Exception\ShopDefinitionNotFound; use PrestaShop\PrestaShop\Core\Domain\Shop\ValueObject\ShopId; use PrestaShop\PrestaShop\Core\Exception\CoreException; use PrestaShop\PrestaShop\Core\Exception\InvalidArgumentException; use PrestaShopDatabaseException; use PrestaShopException; use Shop; /** * This abstract class is an extension of the AbstractObjectModelRepository that provides additional helper functions * to deal with multi shop entities. It provides additional function which rely on mandatory shop IDs. When you use * this class you shouldn't rely on single shop functions anymore, your repository should be oriented as a multi shop * one and always require some shop parameters (single shop becomes only an edge case of you generic multi shop * behaviour). */ class AbstractMultiShopObjectModelRepository extends AbstractObjectModelRepository { /** * @param int $id * @param string $objectModelClass * @param string $exceptionClass * @param ShopId $shopId * @param string $shopAssociationClass * * @return ObjectModel * * @throws CoreException * @throws ShopAssociationNotFound */ protected function getObjectModelForShop(int $id, string $objectModelClass, string $exceptionClass, ShopId $shopId, string $shopAssociationClass = ShopAssociationNotFound::class): ObjectModel { $objectModel = $this->fetchObjectModel($id, $objectModelClass, $exceptionClass, $shopId->getValue()); // The object is fetched before checking the association, so that the NotFoundException has the priority over the NoAssociationException $this->checkShopAssociation($id, $objectModelClass, $shopId, $shopAssociationClass); // Force id_shop_list right away so that DB modification use the appropriate shop and not the one from context $objectModel->id_shop_list = [$shopId->getValue()]; return $objectModel; } /** * @param ObjectModel $objectModel * @param ShopId[] $shopIds * @param string $exceptionClass * @param int $errorCode * * @return int */ protected function addObjectModelToShops(ObjectModel $objectModel, array $shopIds, string $exceptionClass, int $errorCode = 0): int { // Force internal shop list which is used as an override of the one from Context when generating the SQL queries // this way we can control exactly which shop is updated $objectModel->id_shop_list = array_map(function (ShopId $shopId): int { return $shopId->getValue(); }, $shopIds); return $this->addObjectModel($objectModel, $exceptionClass, $errorCode); } /** * @param ObjectModel $objectModel * @param ShopId[] $shopIds * @param string $exceptionClass * @param int $errorCode * * @throws CoreException */ protected function updateObjectModelForShops( ObjectModel $objectModel, array $shopIds, string $exceptionClass, int $errorCode = 0 ): void { // Force internal shop list which is used as an override of the one from Context when generating the SQL queries // this way we can control exactly which shop is updated $objectModel->id_shop_list = array_map(function (ShopId $shopId): int { return $shopId->getValue(); }, $shopIds); $this->updateObjectModel($objectModel, $exceptionClass, $errorCode); } /** * @param ObjectModel $objectModel * @param array $propertiesToUpdate * @param ShopId[] $shopIds * @param string $exceptionClass * @param int $errorCode * * @throws CoreException */ protected function partiallyUpdateObjectModelForShops( ObjectModel $objectModel, array $propertiesToUpdate, array $shopIds, string $exceptionClass, int $errorCode = 0 ): void { $objectModel->setFieldsToUpdate($this->formatPropertiesToUpdate($propertiesToUpdate)); $this->updateObjectModelForShops($objectModel, $shopIds, $exceptionClass, $errorCode); } /** * @param int $id * @param string $objectModelClassName * @param ShopId $shopId * * @return bool */ protected function hasShopAssociation(int $id, string $objectModelClassName, ShopId $shopId): bool { $modelDefinition = $objectModelClassName::$definition; $objectTable = $modelDefinition['table']; $primaryColumn = $modelDefinition['primary']; $query = new DbQuery(); if (Shop::isTableAssociated($objectTable)) { $query ->select('e.`' . bqSQL($primaryColumn) . '` as id') ->from(bqSQL($objectTable) . '_shop', 'e') ->where('e.`' . bqSQL($primaryColumn) . '` = ' . $id) ->where('e.`id_shop` = ' . $shopId->getValue()) ; } elseif (!empty($modelDefinition['multilang_shop'])) { $query ->select('e.`' . bqSQL($primaryColumn) . '` as id') ->from(bqSQL($objectTable) . '_lang', 'e') ->where('e.`' . bqSQL($primaryColumn) . '` = ' . $id) ->where('e.`id_shop` = ' . $shopId->getValue()) ; } else { throw new ShopDefinitionNotFound(sprintf( 'Entity %s has no multishop feature', $objectModelClassName )); } try { $row = Db::getInstance()->getRow($query, false); } catch (PrestaShopDatabaseException|PrestaShopException $e) { $row = false; } return !empty($row['id']); } /** * @param int $id * @param string $objectModelClassName * @param ShopId $shopId * @param string $shopAssociationExceptionClass * * @throws ShopAssociationNotFound */ protected function checkShopAssociation( int $id, string $objectModelClassName, ShopId $shopId, string $shopAssociationExceptionClass = ShopAssociationNotFound::class ): void { if (!$this->hasShopAssociation($id, $objectModelClassName, $shopId)) { throw new $shopAssociationExceptionClass(sprintf( 'Could not find association between %s %d and Shop %d', $objectModelClassName, $id, $shopId->getValue() )); } } /** * @param ObjectModel $objectModel * @param ShopId[] $shopIds * @param string $exceptionClass * @param int $errorCode * * @throws CoreException */ protected function deleteObjectModelFromShops(ObjectModel $objectModel, array $shopIds, string $exceptionClass, int $errorCode = 0): void { if (empty($shopIds)) { throw new InvalidArgumentException('The shopIds should not be empty'); } try { // Force internal shop list which is used as an override of the one from Context when generating the SQL queries // this way we can control exactly which shop is deleted $objectModel->id_shop_list = array_map(static function (ShopId $shopId): int { return $shopId->getValue(); }, $shopIds); if (!$objectModel->delete()) { throw new $exceptionClass( sprintf('Failed to delete %s #%d', get_class($objectModel), $objectModel->id), $errorCode ); } } catch (PrestaShopException $e) { throw new CoreException( sprintf( 'Error occurred when trying to delete %s #%d [%s]', get_class($objectModel), $objectModel->id, $e->getMessage() ), 0, $e ); } } }