diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 6899c65..f364b17 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -3,15 +3,12 @@ on: [push, pull_request] jobs: # Check there is no syntax errors in the project php-linter: - name: PHP Syntax check 5.6 => 8.1 + name: PHP Syntax check 7.2 => 8.1 runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v2.0.0 - - name: PHP syntax checker 5.6 - uses: prestashop/github-action-php-lint/5.6@master - - name: PHP syntax checker 7.2 uses: prestashop/github-action-php-lint/7.2@master @@ -58,7 +55,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - presta-versions: ['1.7.1.2', '1.7.2.5', '1.7.3.4', '1.7.4.4', '1.7.5.1', '1.7.6', '1.7.7', '1.7.8', 'latest'] + presta-versions: ['1.7.7', '1.7.8', 'latest'] steps: - name: Setup PHP uses: shivammathur/setup-php@v2 diff --git a/README.md b/README.md index f0c0398..b63ba37 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,26 @@ # Category tree links +[![PHP tests](https://github.com/PrestaShop/ps_categorytree/actions/workflows/php.yml/badge.svg)](https://github.com/PrestaShop/ps_categorytree/actions/workflows/php.yml) +[![Latest Stable Version](https://poser.pugx.org/PrestaShop/ps_categorytree/v)](//packagist.org/packages/PrestaShop/ps_categorytree) +[![Total Downloads](https://poser.pugx.org/PrestaShop/ps_categorytree/downloads)](//packagist.org/packages/PrestaShop/ps_categorytree) +[![GitHub license](https://img.shields.io/github/license/PrestaShop/ps_categorytree)](https://github.com/PrestaShop/ps_categorytree/LICENSE.md) + ## About Help navigation on your store, show your visitors current category and subcategories. +## Compatibility + +PrestaShop: `1.7.7.0` or later + +## How to test + +Link to specs: https://docs.prestashop-project.org/functional-documentation/functional-documentation/ux-ui/back-office/improve/modules/category-tree-links-ps_categorytree + +Link to test scenario: https://build.prestashop-project.org/test-scenarios/scenarios/modules/ps-categorytree.html + +In `BO > Modules`, configure ps_categorytree module - choose a category, set the maximum depth of category sublevels displayed in the block, select an option of sort by (name/position), select an option of sort order (descending /ascending). In FO as a visitor, verify that the current category and subcategories match the configurations in BO. + ## Reporting issues You can report issues with this module in the main PrestaShop repository. [Click here to report an issue][report-issue]. @@ -26,6 +43,6 @@ Just make sure to follow our [contribution guidelines][contribution-guidelines]. This module is released under the [Academic Free License 3.0][AFL-3.0] [report-issue]: https://github.com/PrestaShop/PrestaShop/issues/new/choose -[prestashop]: https://www.prestashop.com/ +[prestashop]: https://www.prestashop-project.org/ [contribution-guidelines]: https://devdocs.prestashop.com/1.7/contribute/contribution-guidelines/project-modules/ [AFL-3.0]: https://opensource.org/licenses/AFL-3.0 diff --git a/category-tree-branch.tpl b/category-tree-branch.tpl deleted file mode 100644 index b313f02..0000000 --- a/category-tree-branch.tpl +++ /dev/null @@ -1,40 +0,0 @@ -{** - * Copyright since 2007 PrestaShop SA and Contributors - * PrestaShop is an International Registered Trademark & Property of PrestaShop SA - * - * NOTICE OF LICENSE - * - * This source file is subject to the Academic Free License 3.0 (AFL-3.0) - * that is bundled with this package in the file LICENSE.md. - * It is also available through the world-wide-web at this URL: - * https://opensource.org/licenses/AFL-3.0 - * If you did not receive a copy of the license and are unable to - * obtain it through the world-wide-web, please send an email - * to license@prestashop.com so we can send you a copy immediately. - * - * DISCLAIMER - * - * Do not edit or add to this file if you wish to upgrade PrestaShop to newer - * versions in the future. If you wish to customize PrestaShop for your - * needs please refer to https://devdocs.prestashop.com/ for more information. - * - * @author PrestaShop SA and Contributors - * @copyright Since 2007 PrestaShop SA and Contributors - * @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0) - *} - -
  • - {$node.name|escape:'html':'UTF-8'} - {if $node.children|@count > 0} - - {/if} -
  • diff --git a/config.xml b/config.xml index f0e3b25..237b700 100755 --- a/config.xml +++ b/config.xml @@ -1,12 +1,11 @@ - ps_categorytree - - - - - - 1 - 1 - - + ps_categorytree + + + + + + 1 + 1 + \ No newline at end of file diff --git a/img/arrow_right_2.png b/img/arrow_right_2.png deleted file mode 100644 index af4864f..0000000 Binary files a/img/arrow_right_2.png and /dev/null differ diff --git a/img/icon/index.php b/img/icon/index.php deleted file mode 100644 index 80602ae..0000000 --- a/img/icon/index.php +++ /dev/null @@ -1,34 +0,0 @@ - - * @copyright Since 2007 PrestaShop SA and Contributors - * @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0) - */ -header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); -header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); - -header('Cache-Control: no-store, no-cache, must-revalidate'); -header('Cache-Control: post-check=0, pre-check=0', false); -header('Pragma: no-cache'); - -header('Location: ../../../../'); -exit; diff --git a/img/icon/open-close.png b/img/icon/open-close.png deleted file mode 100644 index 99683fb..0000000 Binary files a/img/icon/open-close.png and /dev/null differ diff --git a/img/index.php b/img/index.php deleted file mode 100644 index 2ee97ac..0000000 --- a/img/index.php +++ /dev/null @@ -1,34 +0,0 @@ - - * @copyright Since 2007 PrestaShop SA and Contributors - * @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0) - */ -header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); -header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); - -header('Cache-Control: no-store, no-cache, must-revalidate'); -header('Cache-Control: post-check=0, pre-check=0', false); -header('Pragma: no-cache'); - -header('Location: ../../../'); -exit; diff --git a/ps_categorytree.php b/ps_categorytree.php index 869582c..6e9efa9 100644 --- a/ps_categorytree.php +++ b/ps_categorytree.php @@ -31,11 +31,6 @@ class Ps_CategoryTree extends Module implements WidgetInterface { - /** - * @var string Name of the module running on PS 1.6.x. Used for data migration. - */ - const PS_16_EQUIVALENT_MODULE = 'blockcategories'; - /** * @var int A way to display the category tree: Home category */ @@ -60,7 +55,7 @@ public function __construct() { $this->name = 'ps_categorytree'; $this->tab = 'front_office_features'; - $this->version = '2.0.3'; + $this->version = '3.0.0'; $this->author = 'PrestaShop'; $this->bootstrap = true; @@ -68,41 +63,12 @@ public function __construct() $this->displayName = $this->trans('Category tree links', [], 'Modules.Categorytree.Admin'); $this->description = $this->trans('Help navigation on your store, show your visitors current category and subcategories.', [], 'Modules.Categorytree.Admin'); - $this->ps_versions_compliancy = ['min' => '1.7.1.0', 'max' => _PS_VERSION_]; + $this->ps_versions_compliancy = ['min' => '1.7.7.0', 'max' => _PS_VERSION_]; } public function install() { - // If the PS 1.6 module wasn't here, set the default values - if (!$this->uninstallPrestaShop16Module()) { - Configuration::updateValue('BLOCK_CATEG_MAX_DEPTH', 4); - Configuration::updateValue('BLOCK_CATEG_ROOT_CATEGORY', 1); - } - - return parent::install() - && $this->registerHook('displayLeftColumn'); - } - - /** - * Migrate data from 1.6 equivalent module (if applicable), then uninstall - */ - public function uninstallPrestaShop16Module() - { - if (!Module::isInstalled(self::PS_16_EQUIVALENT_MODULE)) { - return false; - } - $oldModule = Module::getInstanceByName(self::PS_16_EQUIVALENT_MODULE); - if ($oldModule) { - // This closure calls the parent class to prevent data to be erased - // It allows the new module to be configured without migration - $parentUninstallClosure = function () { - return parent::uninstall(); - }; - $parentUninstallClosure = $parentUninstallClosure->bindTo($oldModule, get_class($oldModule)); - $parentUninstallClosure(); - } - - return true; + return parent::install() && $this->registerHook('displayLeftColumn'); } public function uninstall() @@ -136,71 +102,53 @@ public function getContent() return $output . $this->renderForm(); } - private function getCategories($category) + /** + * Format category into an array compatible with existing templates. + */ + private function formatCategory($rawCategory, $idsOfCategoriesInPath): array { - $range = ''; - $maxdepth = Configuration::get('BLOCK_CATEG_MAX_DEPTH'); - if (Validate::isLoadedObject($category)) { - if ($maxdepth > 0) { - $maxdepth += $category->level_depth; + $children = []; + if (!empty($rawCategory['children'])) { + foreach ($rawCategory['children'] as $k => $v) { + $children[$k] = $this->formatCategory($v, $idsOfCategoriesInPath); } - $range = 'AND nleft >= ' . (int) $category->nleft . ' AND nright <= ' . (int) $category->nright; - } - - $resultIds = []; - $resultParents = []; - $result = Db::getInstance((bool) _PS_USE_SQL_SLAVE_)->executeS(' - SELECT c.id_parent, c.id_category, cl.name, cl.description, cl.link_rewrite - FROM `' . _DB_PREFIX_ . 'category` c - INNER JOIN `' . _DB_PREFIX_ . 'category_lang` cl ON (c.`id_category` = cl.`id_category` AND cl.`id_lang` = ' . (int) $this->context->language->id . Shop::addSqlRestrictionOnLang('cl') . ') - INNER JOIN `' . _DB_PREFIX_ . 'category_shop` cs ON (cs.`id_category` = c.`id_category` AND cs.`id_shop` = ' . (int) $this->context->shop->id . ') - WHERE (c.`active` = 1 OR c.`id_category` = ' . (int) Configuration::get('PS_HOME_CATEGORY') . ') - AND c.`id_category` != ' . (int) Configuration::get('PS_ROOT_CATEGORY') . ' - ' . ((int) $maxdepth != 0 ? ' AND `level_depth` <= ' . (int) $maxdepth : '') . ' - ' . $range . ' - AND c.id_category IN ( - SELECT id_category - FROM `' . _DB_PREFIX_ . 'category_group` - WHERE `id_group` IN (' . implode(', ', Customer::getGroupsStatic((int) $this->context->customer->id)) . ') - ) - ORDER BY `level_depth` ASC, ' . (Configuration::get('BLOCK_CATEG_SORT') ? 'cl.`name`' : 'cs.`position`') . ' ' . (Configuration::get('BLOCK_CATEG_SORT_WAY') ? 'DESC' : 'ASC')); - foreach ($result as &$row) { - $resultParents[$row['id_parent']][] = &$row; - $resultIds[$row['id_category']] = &$row; } - return $this->getTree($resultParents, $resultIds, $maxdepth, ($category ? $category->id : null)); + return [ + 'id' => $rawCategory['id_category'], + 'link' => $this->context->link->getCategoryLink($rawCategory['id_category'], $rawCategory['link_rewrite']), + 'name' => $rawCategory['name'], + 'desc' => $rawCategory['description'], + 'children' => $children, + 'in_path' => in_array($rawCategory['id_category'], $idsOfCategoriesInPath), + ]; } - public function getTree($resultParents, $resultIds, $maxDepth, $id_category = null, $currentDepth = 0) + private function getCategories($category): array { - if (is_null($id_category)) { - $id_category = $this->context->shop->getCategory(); + // Determine max depth to get categories + $maxdepth = (int) Configuration::get('BLOCK_CATEG_MAX_DEPTH'); + if ($maxdepth > 0) { + $maxdepth += $category->level_depth; } - $children = []; + // Define filters to get categories + $groups = Customer::getGroupsStatic((int) $this->context->customer->id); + $sqlFilter = $maxdepth ? 'AND c.`level_depth` <= ' . (int) $maxdepth : ''; + $orderBy = ' ORDER BY c.`level_depth` ASC, ' . (Configuration::get('BLOCK_CATEG_SORT') ? 'cl.`name`' : 'category_shop.`position`') . ' ' . (Configuration::get('BLOCK_CATEG_SORT_WAY') ? 'DESC' : 'ASC'); - if (isset($resultParents[$id_category]) && count($resultParents[$id_category]) && ($maxDepth == 0 || $currentDepth < $maxDepth)) { - foreach ($resultParents[$id_category] as $subcat) { - $children[] = $this->getTree($resultParents, $resultIds, $maxDepth, $subcat['id_category'], $currentDepth + 1); - } - } + // Retrieve them using the built in method + $categories = Category::getNestedCategories($category->id, $this->context->language->id, true, $groups, true, $sqlFilter, $orderBy); - if (isset($resultIds[$id_category])) { - $link = $this->context->link->getCategoryLink($id_category, $resultIds[$id_category]['link_rewrite']); - $name = $resultIds[$id_category]['name']; - $desc = $resultIds[$id_category]['description']; - } else { - $link = $name = $desc = ''; + // Get path to current category so we can use it for marking + $idsOfCategoriesInPath = $this->getIdsOfCategoriesInPathToCurrentCategory(); + + // And do our formatting + foreach ($categories as $k => $v) { + $categories[$k] = $this->formatCategory($v, $idsOfCategoriesInPath); } - return [ - 'id' => $id_category, - 'link' => $link, - 'name' => $name, - 'desc' => $desc, - 'children' => $children, - ]; + return array_shift($categories); } public function renderForm() @@ -310,23 +258,8 @@ public function getConfigFieldsValues() ]; } - public function setLastVisitedCategory() - { - if (method_exists($this->context->controller, 'getCategory') && ($category = $this->context->controller->getCategory())) { - $this->context->cookie->last_visited_category = $category->id; - } elseif (method_exists($this->context->controller, 'getProduct') && ($product = $this->context->controller->getProduct())) { - if (!isset($this->context->cookie->last_visited_category) - || !Product::idIsOnCategoryId($product->id, [['id_category' => $this->context->cookie->last_visited_category]]) - || !Category::inShopStatic($this->context->cookie->last_visited_category, $this->context->shop) - ) { - $this->context->cookie->last_visited_category = (int) $product->id_category_default; - } - } - } - public function renderWidget($hookName = null, array $configuration = []) { - $this->setLastVisitedCategory(); $this->smarty->assign($this->getWidgetVariables($hookName, $configuration)); return $this->fetch('module:ps_categorytree/views/templates/hook/ps_categorytree.tpl'); @@ -334,21 +267,104 @@ public function renderWidget($hookName = null, array $configuration = []) public function getWidgetVariables($hookName = null, array $configuration = []) { - if (Configuration::get('BLOCK_CATEG_ROOT_CATEGORY') && !empty($this->context->cookie->last_visited_category) && $this->context->controller instanceof CategoryController) { - $category = new Category($this->context->cookie->last_visited_category, $this->context->language->id); - } else { - $category = new Category((int) Configuration::get('PS_HOME_CATEGORY'), $this->context->language->id); - } - - if (Configuration::get('BLOCK_CATEG_ROOT_CATEGORY') == static::CATEGORY_ROOT_PARENT && !$category->is_root_category && $category->id_parent) { - $category = new Category($category->id_parent, $this->context->language->id); - } elseif (Configuration::get('BLOCK_CATEG_ROOT_CATEGORY') == static::CATEGORY_ROOT_CURRENT_PARENT && !$category->is_root_category && !$category->getSubCategories($category->id, true)) { - $category = new Category($category->id_parent, $this->context->language->id); + switch (Configuration::get('BLOCK_CATEG_ROOT_CATEGORY')) { + // Always the home category + case static::CATEGORY_ROOT_HOME: + $rootCategory = $this->getHomeCategory(); + break; + // Always the current category + case static::CATEGORY_ROOT_CURRENT: + $rootCategory = $this->getCurrentCategory(); + break; + // Always the parent category + case static::CATEGORY_ROOT_PARENT: + $rootCategory = $this->tryToGetParentCategoryIfAvailable($this->getCurrentCategory()); + break; + // Current category, unless it has no subcategories, in which case the parent category of the current category is used + case static::CATEGORY_ROOT_CURRENT_PARENT: + $rootCategory = $this->getCurrentCategory(); + if (!$rootCategory->getSubCategories($rootCategory->id, true)) { + $rootCategory = $this->tryToGetParentCategoryIfAvailable($rootCategory); + } + break; + default: + $rootCategory = $this->getHomeCategory(); } return [ - 'categories' => $this->getCategories($category), - 'currentCategory' => $category->id, + 'categories' => $this->getCategories($rootCategory), + 'currentCategory' => $rootCategory->id, ]; } + + /* + * Tries to retrieve current category from the context. In case of category controller, it's the category. + * In case of product controller, it's either the default category of the product, or the category the customer + * came from. This is resolved by the ProductController. + */ + private function getCurrentCategory(): Category + { + /* + * We check several things: + * If the controller has the method + * If we are on the correct controller + * If we have some sensible data and the category is properly loaded + */ + if ( + !method_exists($this->context->controller, 'getCategory') || + (!$this->context->controller instanceof CategoryController && !$this->context->controller instanceof ProductController) || + empty($this->context->controller->getCategory()) || + !Validate::isLoadedObject($this->context->controller->getCategory()) + ) { + return $this->getHomeCategory(); + } + + return $this->context->controller->getCategory(); + } + + /* + * Tries to get a parent of the current category. + * If we are already on the top of the tree, it will return the input. + * + * Three cases can happen: + * - This category has a normal active parent + * - This category has a disabled parent + * - This category is already the home category + */ + private function tryToGetParentCategoryIfAvailable($category): Category + { + // If we are already on the top of the tree, nothing to do here + if ($category->is_root_category || !$category->id_parent || $category->id == Configuration::get('PS_HOME_CATEGORY')) { + return $category; + } + + // We try to load the parent + $parentCategory = new Category($category->id_parent, $this->context->language->id); + + // If the parent is malfunctioned somehow, we can't do anything and we return the home category + if (!Validate::isLoadedObject($parentCategory)) { + return $this->getHomeCategory(); + } + + // Now we have a valid parent category, let's check it. It must be active, accessible and belong to the shop. + // Same conditions as in CategoryController. If it fails, we select the next parent. + if (!$parentCategory->active || !$category->checkAccess((int) $this->context->customer->id) || !$category->existsInShop($this->context->shop->id)) { + return $this->tryToGetParentCategoryIfAvailable($parentCategory); + } + + return $parentCategory; + } + + private function getIdsOfCategoriesInPathToCurrentCategory(): array + { + // Call built in method to retrieve all parents, including the current category + $categories = $this->getCurrentCategory()->getParentsCategories(); + + return array_column($categories, 'id_category'); + } + + private function getHomeCategory(): Category + { + return new Category((int) Configuration::get('PS_HOME_CATEGORY'), $this->context->language->id); + } } diff --git a/sort_alphabet.png b/sort_alphabet.png deleted file mode 100644 index e00881b..0000000 Binary files a/sort_alphabet.png and /dev/null differ diff --git a/sort_number.png b/sort_number.png deleted file mode 100644 index 8508625..0000000 Binary files a/sort_number.png and /dev/null differ diff --git a/tests/phpstan/phpstan-1.7.1.2.neon b/tests/phpstan/phpstan-1.7.1.2.neon deleted file mode 100644 index 49236e7..0000000 --- a/tests/phpstan/phpstan-1.7.1.2.neon +++ /dev/null @@ -1,10 +0,0 @@ -includes: - - %currentWorkingDirectory%/tests/phpstan/phpstan.neon - -parameters: - ignoreErrors: - - '#Access to an undefined property Cookie::\$last_visited_category.#' - - '#Call to method assign\(\) on an unknown class Smarty_Data.#' - - '#Parameter \#1 \$idCategory of class Category constructor expects null, int given.#' - - '#Parameter \#1 \$idCategory of class Category constructor expects null, int|int<1, max> given.#' - - '#Parameter \#1 \$idCategory of class Category constructor expects null, mixed given.#' diff --git a/tests/phpstan/phpstan-1.7.2.5.neon b/tests/phpstan/phpstan-1.7.2.5.neon deleted file mode 100644 index 49236e7..0000000 --- a/tests/phpstan/phpstan-1.7.2.5.neon +++ /dev/null @@ -1,10 +0,0 @@ -includes: - - %currentWorkingDirectory%/tests/phpstan/phpstan.neon - -parameters: - ignoreErrors: - - '#Access to an undefined property Cookie::\$last_visited_category.#' - - '#Call to method assign\(\) on an unknown class Smarty_Data.#' - - '#Parameter \#1 \$idCategory of class Category constructor expects null, int given.#' - - '#Parameter \#1 \$idCategory of class Category constructor expects null, int|int<1, max> given.#' - - '#Parameter \#1 \$idCategory of class Category constructor expects null, mixed given.#' diff --git a/tests/phpstan/phpstan-1.7.3.4.neon b/tests/phpstan/phpstan-1.7.3.4.neon deleted file mode 100644 index 49236e7..0000000 --- a/tests/phpstan/phpstan-1.7.3.4.neon +++ /dev/null @@ -1,10 +0,0 @@ -includes: - - %currentWorkingDirectory%/tests/phpstan/phpstan.neon - -parameters: - ignoreErrors: - - '#Access to an undefined property Cookie::\$last_visited_category.#' - - '#Call to method assign\(\) on an unknown class Smarty_Data.#' - - '#Parameter \#1 \$idCategory of class Category constructor expects null, int given.#' - - '#Parameter \#1 \$idCategory of class Category constructor expects null, int|int<1, max> given.#' - - '#Parameter \#1 \$idCategory of class Category constructor expects null, mixed given.#' diff --git a/tests/phpstan/phpstan-1.7.4.4.neon b/tests/phpstan/phpstan-1.7.4.4.neon deleted file mode 100644 index 49236e7..0000000 --- a/tests/phpstan/phpstan-1.7.4.4.neon +++ /dev/null @@ -1,10 +0,0 @@ -includes: - - %currentWorkingDirectory%/tests/phpstan/phpstan.neon - -parameters: - ignoreErrors: - - '#Access to an undefined property Cookie::\$last_visited_category.#' - - '#Call to method assign\(\) on an unknown class Smarty_Data.#' - - '#Parameter \#1 \$idCategory of class Category constructor expects null, int given.#' - - '#Parameter \#1 \$idCategory of class Category constructor expects null, int|int<1, max> given.#' - - '#Parameter \#1 \$idCategory of class Category constructor expects null, mixed given.#' diff --git a/tests/phpstan/phpstan-1.7.5.1.neon b/tests/phpstan/phpstan-1.7.5.1.neon deleted file mode 100644 index 49236e7..0000000 --- a/tests/phpstan/phpstan-1.7.5.1.neon +++ /dev/null @@ -1,10 +0,0 @@ -includes: - - %currentWorkingDirectory%/tests/phpstan/phpstan.neon - -parameters: - ignoreErrors: - - '#Access to an undefined property Cookie::\$last_visited_category.#' - - '#Call to method assign\(\) on an unknown class Smarty_Data.#' - - '#Parameter \#1 \$idCategory of class Category constructor expects null, int given.#' - - '#Parameter \#1 \$idCategory of class Category constructor expects null, int|int<1, max> given.#' - - '#Parameter \#1 \$idCategory of class Category constructor expects null, mixed given.#' diff --git a/tests/phpstan/phpstan-1.7.6.neon b/tests/phpstan/phpstan-1.7.6.neon deleted file mode 100644 index 49236e7..0000000 --- a/tests/phpstan/phpstan-1.7.6.neon +++ /dev/null @@ -1,10 +0,0 @@ -includes: - - %currentWorkingDirectory%/tests/phpstan/phpstan.neon - -parameters: - ignoreErrors: - - '#Access to an undefined property Cookie::\$last_visited_category.#' - - '#Call to method assign\(\) on an unknown class Smarty_Data.#' - - '#Parameter \#1 \$idCategory of class Category constructor expects null, int given.#' - - '#Parameter \#1 \$idCategory of class Category constructor expects null, int|int<1, max> given.#' - - '#Parameter \#1 \$idCategory of class Category constructor expects null, mixed given.#'