1<?php
2/**
3 * 2007-2016 PrestaShop
4 *
5 * thirty bees is an extension to the PrestaShop e-commerce software developed by PrestaShop SA
6 * Copyright (C) 2017-2018 thirty bees
7 *
8 * NOTICE OF LICENSE
9 *
10 * This source file is subject to the Open Software License (OSL 3.0)
11 * that is bundled with this package in the file LICENSE.txt.
12 * It is also available through the world-wide-web at this URL:
13 * http://opensource.org/licenses/osl-3.0.php
14 * If you did not receive a copy of the license and are unable to
15 * obtain it through the world-wide-web, please send an email
16 * to license@thirtybees.com so we can send you a copy immediately.
17 *
18 * DISCLAIMER
19 *
20 * Do not edit or add to this file if you wish to upgrade PrestaShop to newer
21 * versions in the future. If you wish to customize PrestaShop for your
22 * needs please refer to https://www.thirtybees.com for more information.
23 *
24 * @author    thirty bees <contact@thirtybees.com>
25 * @author    PrestaShop SA <contact@prestashop.com>
26 * @copyright 2017-2018 thirty bees
27 * @copyright 2007-2016 PrestaShop SA
28 * @license   http://opensource.org/licenses/osl-3.0.php  Open Software License (OSL 3.0)
29 *  PrestaShop is an internationally registered trademark & property of PrestaShop SA
30 */
31
32/**
33 * Class AttributeGroupCore
34 *
35 * @since 1.0.0
36 */
37class AttributeGroupCore extends ObjectModel
38{
39    // @codingStandardsIgnoreStart
40    /**
41     * @see ObjectModel::$definition
42     */
43    public static $definition = [
44        'table'     => 'attribute_group',
45        'primary'   => 'id_attribute_group',
46        'multilang' => true,
47        'fields'    => [
48            'is_color_group' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'],
49            'group_type'     => ['type' => self::TYPE_STRING, 'required' => true],
50            'position'       => ['type' => self::TYPE_INT, 'validate' => 'isInt'],
51
52            /* Lang fields */
53            'name'           => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isGenericName', 'required' => true, 'size' => 128],
54            'public_name'    => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isGenericName', 'required' => true, 'size' => 64],
55        ],
56    ];
57    /** @var string Name */
58    public $name;
59    /** @var bool $is_color_group */
60    public $is_color_group;
61    /** @var int $position */
62    public $position;
63    /** @var string $group_type */
64    public $group_type;
65    /** @var string Public Name */
66    public $public_name;
67    protected $webserviceParameters = [
68        'objectsNodeName' => 'product_options',
69        'objectNodeName'  => 'product_option',
70        'fields'          => [],
71        'associations'    => [
72            'product_option_values' => [
73                'resource' => 'product_option_value',
74                'fields'   => [
75                    'id' => [],
76                ],
77            ],
78        ],
79    ];
80    // @codingStandardsIgnoreEnd
81
82    /**
83     * Get all attributes for a given language / group
84     *
85     * @param int  $idLang           Language id
86     * @param bool $idAttributeGroup Attribute group id
87     *
88     * @return array Attributes
89     * @throws PrestaShopDatabaseException
90     * @throws PrestaShopException
91     */
92    public static function getAttributes($idLang, $idAttributeGroup)
93    {
94        if (!Combination::isFeatureActive()) {
95            return [];
96        }
97
98        return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(
99            (new DbQuery())
100                ->select('*')
101                ->from('attribute', 'a')
102                ->join(Shop::addSqlAssociation('attribute', 'a'))
103                ->leftJoin('attribute_lang', 'al', 'a.`id_attribute` = al.`id_attribute` AND al.`id_lang` = '.(int) $idLang)
104                ->where('a.`id_attribute_group` = '.(int) $idAttributeGroup)
105                ->orderBy('`position` ASC')
106        );
107    }
108
109    /**
110     * Get all attributes groups for a given language
111     *
112     * @param int $idLang Language id
113     *
114     * @return array Attributes groups
115     * @throws PrestaShopDatabaseException
116     * @throws PrestaShopException
117     */
118    public static function getAttributesGroups($idLang)
119    {
120        if (!Combination::isFeatureActive()) {
121            return [];
122        }
123
124        return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(
125            (new DbQuery())
126                ->select('DISTINCT agl.`name`, ag.*, agl.*')
127                ->from('attribute_group', 'ag')
128                ->join(Shop::addSqlAssociation('attribute_group', 'ag'))
129                ->leftJoin('attribute_group_lang', 'agl', 'ag.`id_attribute_group` = agl.`id_attribute_group` AND agl.`id_lang` = '.(int) $idLang)
130                ->orderBy('agl.`name` ASC')
131        );
132    }
133
134    /**
135     * @param bool $autoDate
136     * @param bool $nullValues
137     *
138     * @return bool
139     *
140     * @since   1.0.0
141     * @version 1.0.0 Initial version
142     * @throws PrestaShopException
143     */
144    public function add($autoDate = true, $nullValues = false)
145    {
146        if ($this->group_type == 'color') {
147            $this->is_color_group = 1;
148        } else {
149            $this->is_color_group = 0;
150        }
151
152        if ($this->position <= 0) {
153            $this->position = AttributeGroup::getHigherPosition() + 1;
154        }
155
156        $return = parent::add($autoDate, true);
157        Hook::exec('actionAttributeGroupSave', ['id_attribute_group' => $this->id]);
158
159        return $return;
160    }
161
162    /**
163     * getHigherPosition
164     *
165     * Get the higher group attribute position
166     *
167     * @return int $position
168     * @throws PrestaShopException
169     */
170    public static function getHigherPosition()
171    {
172        $position = (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue(
173            (new DbQuery())
174                ->select('MAX(`position`)')
175                ->from('attribute_group')
176        );
177
178        if (!$position) {
179            return -1;
180        }
181
182        return $position;
183    }
184
185    /**
186     * @param bool $nullValues
187     *
188     * @return bool
189     *
190     * @since   1.0.0
191     * @version 1.0.0 Initial version
192     */
193    public function update($nullValues = false)
194    {
195        if ($this->group_type == 'color') {
196            $this->is_color_group = 1;
197        } else {
198            $this->is_color_group = 0;
199        }
200
201        $return = parent::update($nullValues);
202        Hook::exec('actionAttributeGroupSave', ['id_attribute_group' => $this->id]);
203
204        return $return;
205    }
206
207    /**
208     * Delete several objects from database
209     *
210     * return boolean Deletion result
211     *
212     * @since   1.0.0
213     * @version 1.0.0 Initial version
214     *
215     * @param array $selection
216     *
217     * @return bool
218     * @throws PrestaShopDatabaseException
219     */
220    public function deleteSelection($selection)
221    {
222        /* Also delete Attributes */
223        foreach ($selection as $value) {
224            $obj = new AttributeGroup($value);
225            if (!$obj->delete()) {
226                return false;
227            }
228        }
229
230        return true;
231    }
232
233    /**
234     * @return bool
235     *
236     * @since   1.0.0
237     * @version 1.0.0 Initial version
238     * @throws PrestaShopDatabaseException
239     * @throws PrestaShopException
240     */
241    public function delete()
242    {
243        if (!$this->hasMultishopEntries() || Shop::getContext() == Shop::CONTEXT_ALL) {
244            /* Select children in order to find linked combinations */
245            $attributeIds = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(
246                (new DbQuery())
247                    ->select('`id_attribute`')
248                    ->from('attribute')
249                    ->where('`id_attribute_group` = '.(int) $this->id)
250            );
251            if ($attributeIds === false) {
252                return false;
253            }
254            /* Removing attributes to the found combinations */
255            $toRemove = [];
256            foreach ($attributeIds as $attribute) {
257                $toRemove[] = (int) $attribute['id_attribute'];
258            }
259            if (!empty($toRemove)
260                && Db::getInstance()->delete('product_attribute_combination', '`id_attribute` IN ('.implode(',', $toRemove).')') === false
261            ) {
262                return false;
263            }
264            /* Remove combinations if they do not possess attributes anymore */
265            if (!AttributeGroup::cleanDeadCombinations()) {
266                return false;
267            }
268            /* Also delete related attributes */
269            if (count($toRemove)) {
270                if (!Db::getInstance()->delete('attribute_lang', '`id_attribute` IN ('.implode(',', $toRemove).')')
271                    || !Db::getInstance()->delete('attribute_shop', '`id_attribute` IN ('.implode(',', $toRemove).')')
272                    || !Db::getInstance()->delete('attribute', '`id_attribute_group` = '.(int) $this->id)
273                ) {
274                    return false;
275                }
276            }
277            $this->cleanPositions();
278        }
279        $return = parent::delete();
280        if ($return) {
281            Hook::exec('actionAttributeGroupDelete', ['id_attribute_group' => $this->id]);
282        }
283
284        return $return;
285    }
286
287    /**
288     * @return bool
289     *
290     * @throws PrestaShopDatabaseException
291     * @throws PrestaShopException
292     * @since   1.0.0
293     * @version 1.0.0 Initial version
294     */
295    public static function cleanDeadCombinations()
296    {
297        $attributeCombinations = Db::getInstance()->executeS(
298            (new DbQuery())
299                ->select('pac.`id_attribute`, pa.`id_product_attribute`')
300                ->from('product_attribute', 'pa')
301                ->leftJoin('product_attribute_combination', 'pac', 'pa.`id_product_attribute` = pac.`id_product_attribute`')
302        );
303        $toRemove = [];
304        foreach ($attributeCombinations as $attributeCombination) {
305            if ((int) $attributeCombination['id_attribute'] == 0) {
306                $toRemove[] = (int) $attributeCombination['id_product_attribute'];
307            }
308        }
309        $return = true;
310        if (!empty($toRemove)) {
311            foreach ($toRemove as $remove) {
312                $combination = new Combination($remove);
313                $return &= $combination->delete();
314            }
315        }
316
317        return $return;
318    }
319
320    /**
321     * Reorder group attribute position
322     * Call it after deleting a group attribute.
323     *
324     * @return bool $return
325     *
326     * @throws PrestaShopDatabaseException
327     * @throws PrestaShopException
328     * @since   1.0.0
329     * @version 1.0.0 Initial version
330     */
331    public static function cleanPositions()
332    {
333        $return = true;
334        $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(
335            (new DbQuery())
336                ->select('`id_attribute_group`')
337                ->from('attribute_group')
338                ->orderBy('`position`')
339        );
340
341        $i = 0;
342        foreach ($result as $value) {
343            $return = Db::getInstance()->update(
344                'attribute_group',
345                [
346                    'position' => (int) $i++,
347                ],
348                '`id_attribute_group` = '.(int) $value['id_attribute_group']
349            );
350        }
351
352        return $return;
353    }
354
355    /**
356     * @param array $values
357     *
358     * @return bool
359     *
360     * @since   1.0.0
361     * @version 1.0.0 Initial version
362     * @throws PrestaShopDatabaseException
363     * @throws PrestaShopException
364     */
365    public function setWsProductOptionValues($values)
366    {
367        $ids = [];
368        foreach ($values as $value) {
369            $ids[] = intval($value['id']);
370        }
371        Db::getInstance()->delete(
372            'attribute',
373            '`id_attribute_group` = '.(int) $this->id.' AND `id_attribute` NOT IN ('.implode(',', $ids).')'
374        );
375        $ok = true;
376        foreach ($values as $value) {
377            $result = Db::getInstance()->update(
378                'attribute',
379                [
380                    'id_attribute_group' => (int) $this->id,
381                ],
382                '`id_attribute` = '.(int) $value['id']
383            );
384            if ($result === false) {
385                $ok = false;
386            }
387        }
388
389        return $ok;
390    }
391
392    /**
393     * @return array|false|mysqli_result|null|PDOStatement|resource
394     *
395     * @throws PrestaShopDatabaseException
396     * @throws PrestaShopException
397     * @since   1.0.0
398     * @version 1.0.0 Initial version
399     */
400    public function getWsProductOptionValues()
401    {
402        $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(
403            (new DbQuery())
404                ->select('a.`id_attribute` AS `id`')
405                ->from('attribute', 'a')
406                ->join(Shop::addSqlAssociation('attribute', 'a'))
407                ->where('a.`id_attribute_group` = '.(int) $this->id)
408        );
409
410        return $result;
411    }
412
413    /**
414     * Move a group attribute
415     *
416     * @param bool $way      Up (1) or Down (0)
417     * @param int  $position
418     *
419     * @return bool Update result
420     *
421     * @throws PrestaShopDatabaseException
422     * @throws PrestaShopException
423     * @since   1.0.0
424     * @version 1.0.0 Initial version
425     */
426    public function updatePosition($way, $position)
427    {
428        if (!$res = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(
429            (new DbQuery())
430                ->select('ag.`position`, ag.`id_attribute_group`')
431                ->from('attribute_group', 'ag')
432                ->where('ag.`id_attribute_group` = '.(int) Tools::getValue('id_attribute_group', 1))
433                ->orderBy('ag.`position` ASC')
434        )
435        ) {
436            return false;
437        }
438
439        foreach ($res as $groupAttribute) {
440            if ((int) $groupAttribute['id_attribute_group'] == (int) $this->id) {
441                $movedGroupAttribute = $groupAttribute;
442            }
443        }
444
445        if (!isset($movedGroupAttribute) || !isset($position)) {
446            return false;
447        }
448
449        // < and > statements rather than BETWEEN operator
450        // since BETWEEN is treated differently according to databases
451        return Db::getInstance()->update(
452            'attribute_group',
453            [
454                'position' => ['type' => 'sql', 'value' => '`position` '.($way ? '- 1' : '+ 1')],
455            ],
456            '`position` '.($way ? '> '.(int) $movedGroupAttribute['position'].' AND `position` <= '.(int) $position : '< '.(int) $movedGroupAttribute['position'].' AND `position` >= '.(int) $position)
457        ) && Db::getInstance()->update(
458            'attribute_group',
459            [
460                'position' => (int) $position,
461            ],
462            '`id_attribute_group` = '.(int) $movedGroupAttribute['id_attribute_group']
463        );
464    }
465}
466