From 9bd155fba5e4a291c6ad22ed61a380e213d5df1d Mon Sep 17 00:00:00 2001 From: Johan Cwiklinski Date: Wed, 23 Sep 2026 10:42:37 +0200 Subject: [PATCH 1/6] Share list, count and search code between repositories --- .../Controllers/Crud/CategoriesController.php | 2 +- .../Controllers/Crud/ObjectsController.php | 18 +- .../Controllers/Crud/StatusController.php | 2 +- .../Controllers/PdfController.php | 6 +- .../Filters/CategoriesList.php | 6 +- .../Filters/ObjectsList.php | 6 +- lib/GaletteObjectsLend/Filters/StatusList.php | 6 +- lib/GaletteObjectsLend/LendService.php | 13 +- .../Repository/AbstractRepository.php | 191 +++++++ .../Repository/Categories.php | 352 +++---------- lib/GaletteObjectsLend/Repository/Objects.php | 465 +++++------------- lib/GaletteObjectsLend/Repository/Status.php | 317 +++--------- phpstan-baseline.neon | 102 ---- .../tests/units/ObjectsController.php | 2 +- .../Entity/tests/units/LendObject.php | 2 + .../Entity/tests/units/LendRent.php | 4 +- .../Entity/tests/units/LendStatus.php | 4 +- .../Repository/tests/units/Categories.php | 15 +- .../Repository/tests/units/Objects.php | 4 +- .../Repository/tests/units/Status.php | 4 +- .../tests/units/LendService.php | 2 +- 21 files changed, 506 insertions(+), 1017 deletions(-) create mode 100644 lib/GaletteObjectsLend/Repository/AbstractRepository.php diff --git a/lib/GaletteObjectsLend/Controllers/Crud/CategoriesController.php b/lib/GaletteObjectsLend/Controllers/Crud/CategoriesController.php index a9ab5480..88670f7f 100644 --- a/lib/GaletteObjectsLend/Controllers/Crud/CategoriesController.php +++ b/lib/GaletteObjectsLend/Controllers/Crud/CategoriesController.php @@ -89,7 +89,7 @@ public function list(Request $request, Response $response, ?string $option = nul } } - $categories = new Categories($this->zdb, $this->login, $filters); + $categories = new Categories($this->zdb, $this->preferences, $this->login, $filters); $list = $categories->getCategoriesList(true); $this->session->objectslend_filter_categories = $filters; diff --git a/lib/GaletteObjectsLend/Controllers/Crud/ObjectsController.php b/lib/GaletteObjectsLend/Controllers/Crud/ObjectsController.php index 2d0dab15..169530ff 100644 --- a/lib/GaletteObjectsLend/Controllers/Crud/ObjectsController.php +++ b/lib/GaletteObjectsLend/Controllers/Crud/ObjectsController.php @@ -106,7 +106,7 @@ public function list(Request $request, Response $response, ?string $option = nul } $lendsprefs = new Preferences($this->zdb); - $objects = new Objects($this->zdb, $lendsprefs, $filters); + $objects = new Objects($this->zdb, $this->preferences, $this->login, $lendsprefs, $filters); $list = $objects->getObjectsList(true); $this->session->objectslend_filter_objects = $filters; @@ -119,8 +119,8 @@ public function list(Request $request, Response $response, ?string $option = nul $cat_filters->active_filter = Categories::ACTIVE_CATEGORIES; //retrieve only active categories $cat_filters->not_empty = true; //retrieve only categories with objects $cat_filters->setObjectsFilter($filters); //search for categories corresponding to filtered objects - $categories = new Categories($this->zdb, $this->login, $cat_filters); - $categories_list = $categories->getCategoriesList(true, null, false); + $categories = new Categories($this->zdb, $this->preferences, $this->login, $cat_filters); + $categories_list = $categories->getCategoriesList(true, false, false); // display page $this->view->render( @@ -280,7 +280,7 @@ public function edit(Request $request, Response $response, ?int $id = null, stri } unset($this->session->objectslend_object_data); - $categories = new Categories($this->zdb, $this->login); + $categories = new Categories($this->zdb, $this->preferences, $this->login); $categories_list = $categories->getCategoriesList(true); if ($object->getId() !== null) { @@ -291,7 +291,7 @@ public function edit(Request $request, Response $response, ?int $id = null, stri $sfilter = new StatusList(); $sfilter->active_filter = \GaletteObjectsLend\Repository\Status::ACTIVE; - $statuses = new Status($this->zdb, $this->login, $sfilter); + $statuses = new Status($this->zdb, $this->preferences, $this->login, $sfilter); $slist = $statuses->getStatusList(true); $lendsprefs = new Preferences($this->zdb); @@ -560,8 +560,8 @@ public function lend(Request $request, Response $response, string $action, int $ ), 'time' => time(), 'statuses' => ($action == 'take' - ? (new Status($this->zdb, $this->login))->getActiveTakeAwayStatuses() - : (new Status($this->zdb, $this->login))->getActiveStockStatuses()), + ? (new Status($this->zdb, $this->preferences, $this->login))->getActiveTakeAwayStatuses() + : (new Status($this->zdb, $this->preferences, $this->login))->getActiveStockStatuses()), 'lendsprefs' => $lendsprefs->getPreferences(), 'olendsprefs' => $lendsprefs, 'ajax' => $this->isAjax($request), @@ -788,7 +788,7 @@ private function lendResponse(Request $request, Response $response): Response */ private function getLendService(?Preferences $lendsprefs = null): LendService { - return new LendService($this->zdb, $this->login, $lendsprefs ?? new Preferences($this->zdb)); + return new LendService($this->zdb, $this->preferences, $this->login, $lendsprefs ?? new Preferences($this->zdb)); } // /CRUD - Update @@ -878,7 +878,7 @@ protected function doDelete(array $args, array $post): bool $filters = new ObjectsList(); } $lendsprefs = new Preferences($this->zdb); - $objects = new Objects($this->zdb, $lendsprefs, $filters); + $objects = new Objects($this->zdb, $this->preferences, $this->login, $lendsprefs, $filters); if (!is_array($post['id'])) { $ids = (array)$post['id']; diff --git a/lib/GaletteObjectsLend/Controllers/Crud/StatusController.php b/lib/GaletteObjectsLend/Controllers/Crud/StatusController.php index c71e3e06..244b6754 100644 --- a/lib/GaletteObjectsLend/Controllers/Crud/StatusController.php +++ b/lib/GaletteObjectsLend/Controllers/Crud/StatusController.php @@ -87,7 +87,7 @@ public function list(Request $request, Response $response, ?string $option = nul } } - $statuses = new Status($this->zdb, $this->login, $filters); + $statuses = new Status($this->zdb, $this->preferences, $this->login, $filters); $list = $statuses->getStatusList(true); if (count($statuses->getActiveStockStatuses()) == 0) { diff --git a/lib/GaletteObjectsLend/Controllers/PdfController.php b/lib/GaletteObjectsLend/Controllers/PdfController.php index 773208b2..6b234848 100644 --- a/lib/GaletteObjectsLend/Controllers/PdfController.php +++ b/lib/GaletteObjectsLend/Controllers/PdfController.php @@ -37,7 +37,7 @@ class PdfController extends GPdfController public function printObject(Request $request, Response $response, int $id): Response { $lendsprefs = new Preferences($this->zdb); - $object = (new Objects($this->zdb, $lendsprefs))->getWithCurrentRent($id); + $object = (new Objects($this->zdb, $this->preferences, $this->login, $lendsprefs))->getWithCurrentRent($id); $pdf = new PdfObject( $this->zdb, @@ -67,8 +67,8 @@ public function printObjects(Request $request, Response $response): Response if ($filters->orderby !== Objects::ORDERBY_CATEGORY) { $filters->orderby = Objects::ORDERBY_CATEGORY; } - $objects = new Objects($this->zdb, $lendsprefs, $filters); - $list = $objects->getObjectsList(true, null, true, false); + $objects = new Objects($this->zdb, $this->preferences, $this->login, $lendsprefs, $filters); + $list = $objects->getObjectsList(true, true, false); $pdf = new PdfObjects( $this->zdb, diff --git a/lib/GaletteObjectsLend/Filters/CategoriesList.php b/lib/GaletteObjectsLend/Filters/CategoriesList.php index 41af6831..1cbe9d59 100644 --- a/lib/GaletteObjectsLend/Filters/CategoriesList.php +++ b/lib/GaletteObjectsLend/Filters/CategoriesList.php @@ -23,7 +23,6 @@ * @property ?int $active_filter * @property ?bool $not_empty * @property ?ObjectsList $objects_filters - * @property string $query */ class CategoriesList extends Pagination @@ -34,15 +33,13 @@ class CategoriesList extends Pagination private ?bool $not_empty; private ?ObjectsList $objects_filters; - protected string $query; /** @var array */ protected array $categorylist_fields = [ 'filter_str', 'active_filter', 'not_empty', - 'objects_filters', - 'query' + 'objects_filters' ]; /** @@ -118,7 +115,6 @@ public function __set(string $name, mixed $value): void switch ($name) { case 'filter_str': - case 'query': case 'not_empty': $this->$name = $value; break; diff --git a/lib/GaletteObjectsLend/Filters/ObjectsList.php b/lib/GaletteObjectsLend/Filters/ObjectsList.php index 08f4c33f..c244e4ff 100644 --- a/lib/GaletteObjectsLend/Filters/ObjectsList.php +++ b/lib/GaletteObjectsLend/Filters/ObjectsList.php @@ -26,7 +26,6 @@ * @property ?int $active_filter * @property ?int $field_filter * @property array $selected - * @property string $query */ class ObjectsList extends Pagination @@ -39,7 +38,6 @@ class ObjectsList extends Pagination /** @var array */ private array $selected; - protected string $query; /** @var array */ protected array $objectslist_fields = [ @@ -47,8 +45,7 @@ class ObjectsList extends Pagination 'category_filter', 'active_filter', 'field_filter', - 'selected', - 'query' + 'selected' ]; /** @@ -185,7 +182,6 @@ public function __set(string $name, mixed $value): void } break; case 'filter_str': - case 'query': $this->$name = $value; break; case 'category_filter': diff --git a/lib/GaletteObjectsLend/Filters/StatusList.php b/lib/GaletteObjectsLend/Filters/StatusList.php index b369cf63..a60b31f0 100644 --- a/lib/GaletteObjectsLend/Filters/StatusList.php +++ b/lib/GaletteObjectsLend/Filters/StatusList.php @@ -22,7 +22,6 @@ * @property ?string $filter_str * @property ?int $active_filter * @property ?int $stock_filter - * @property string $query */ class StatusList extends Pagination @@ -32,14 +31,12 @@ class StatusList extends Pagination private ?int $active_filter; private ?int $stock_filter; - protected string $query; /** @var array */ protected array $statuslist_fields = [ 'filter_str', 'active_filter', - 'stock_filter', - 'query' + 'stock_filter' ]; /** @@ -106,7 +103,6 @@ public function __set(string $name, mixed $value): void switch ($name) { case 'filter_str': - case 'query': $this->$name = $value; break; case 'active_filter': diff --git a/lib/GaletteObjectsLend/LendService.php b/lib/GaletteObjectsLend/LendService.php index fc87b71b..489372d4 100644 --- a/lib/GaletteObjectsLend/LendService.php +++ b/lib/GaletteObjectsLend/LendService.php @@ -13,6 +13,7 @@ use Analog\Analog; use Galette\Core\Db; use Galette\Core\Login; +use Galette\Core\Preferences as CorePreferences; use Galette\Entity\Adherent; use Galette\Entity\Contribution; use Galette\Entity\ContributionsTypes; @@ -38,12 +39,14 @@ class LendService /** * Constructor * - * @param Db $zdb Database instance - * @param Login $login Logged in instance - * @param Preferences $lendsprefs Plugin preferences + * @param Db $zdb Database instance + * @param CorePreferences $preferences Preferences + * @param Login $login Logged in instance + * @param Preferences $lendsprefs Plugin preferences */ public function __construct( private Db $zdb, + private CorePreferences $preferences, private Login $login, private Preferences $lendsprefs ) { @@ -56,7 +59,7 @@ public function __construct( */ public function getObject(int $id): LendObject { - return (new Objects($this->zdb, $this->lendsprefs))->getWithCurrentRent($id); + return (new Objects($this->zdb, $this->preferences, $this->login, $this->lendsprefs))->getWithCurrentRent($id); } /** @@ -429,7 +432,7 @@ private function refuse(string $log, LendObject $object, string $message): never */ private function getStatuses(): Status { - return new Status($this->zdb, $this->login); + return new Status($this->zdb, $this->preferences, $this->login); } /** diff --git a/lib/GaletteObjectsLend/Repository/AbstractRepository.php b/lib/GaletteObjectsLend/Repository/AbstractRepository.php new file mode 100644 index 00000000..4b303aa2 --- /dev/null +++ b/lib/GaletteObjectsLend/Repository/AbstractRepository.php @@ -0,0 +1,191 @@ + + * + * @template TEntity of object + */ +abstract class AbstractRepository extends Repository +{ + public const int ALL = 0; + public const int ACTIVE = 1; + public const int INACTIVE = 2; + + public const string PK = ''; + /** Table alias used in queries */ + protected const string ALIAS = ''; + + private ?int $count = null; + + /** + * Constructor + * + * @param Db $zdb Database instance + * @param Preferences $preferences Preferences instance + * @param Login $login Logged in instance + * @param string $entity Entity class name, relative to the Entity namespace + * @param Pagination $filters Filtering + */ + public function __construct( + Db $zdb, + Preferences $preferences, + Login $login, + string $entity, + Pagination $filters + ) { + parent::__construct($zdb, $preferences, $login, 'Entity\\' . $entity, 'GaletteObjectsLend', LEND_PREFIX); + $this->filters = $filters; + } + + /** + * Builds the SELECT statement, filtered but neither ordered nor limited + */ + abstract protected function buildSelect(): Select; + + /** + * Builds the order clause + * + * @return array + */ + abstract protected function buildOrderClause(): array; + + /** + * Create an entity from a resultset row + * + * @param ArrayObject $row Resultset row + * + * @return TEntity + */ + abstract protected function createEntity(ArrayObject $row): object; + + /** + * Get the list + * + * @param bool $as_entities Return entities instead of a resultset + * @param bool $count Count all matching rows, for pagination + * @param bool $limit Only retrieve the rows of the current page + * + * @return ($as_entities is true ? array : ResultSet) + */ + protected function fetchList(bool $as_entities, bool $count, bool $limit): array|ResultSet + { + $select = $this->buildSelect(); + $select->order($this->buildOrderClause()); + + if ($count) { + $this->proceedCount($select); + } + if ($limit) { + $this->filters->setLimits($select); + } + + /** @var ResultSet $rows */ + $rows = $this->zdb->execute($select); + + if (!$as_entities) { + return $rows; + } + + $list = []; + foreach ($rows as $row) { + $list[] = $this->createEntity($row); + } + return $list; + } + + /** + * Count rows matching the query + * + * Counting on a subquery keeps grouping and HAVING clauses right. + * + * @param Select $select Original select + */ + private function proceedCount(Select $select): void + { + $counted = clone $select; + $counted->reset(Select::COLUMNS); + $counted->reset(Select::ORDER); + $counted->reset(Select::JOINS); + $counted->columns(['id' => new Expression(static::ALIAS . '.' . static::PK)]); + foreach ($select->joins as $join) { + $counted->join($join['name'], $join['on'], [], $join['type']); + } + + $count_select = new Select(['counted' => $counted]); + $count_select->columns(['count' => new Expression('COUNT(*)')]); + + $result = $this->zdb->execute($count_select)->current(); + $this->count = (int)$result['count']; + $this->filters->setCounter($this->count); + } + + /** + * Get count for current query + */ + public function getCount(): ?int + { + return $this->count; + } + + /** + * Case-insensitive search on a column + * + * @param string $column Qualified column name, as a trusted literal + * @param string $search Searched string + */ + protected function contains(string $column, string $search): PredicateExpression + { + return new PredicateExpression( + 'LOWER(' . $column . ') LIKE ?', + ['%' . mb_strtolower($search) . '%'] + ); + } + + /** + * Filter on activity + * + * @param Select $select Select + * @param int $filter One of ALL, ACTIVE or INACTIVE + */ + protected function whereActive(Select $select, ?int $filter): void + { + if ($filter === self::ACTIVE) { + $select->where([static::ALIAS . '.is_active' => 1]); + } elseif ($filter === self::INACTIVE) { + $select->where([static::ALIAS . '.is_active' => 0]); + } + } + + /** + * Nothing to initialize + * + * @param bool $check_first Check first if it seems initialized + */ + public function installInit(bool $check_first = true): bool + { + return true; + } +} diff --git a/lib/GaletteObjectsLend/Repository/Categories.php b/lib/GaletteObjectsLend/Repository/Categories.php index 1f3336e6..acf9f6d2 100644 --- a/lib/GaletteObjectsLend/Repository/Categories.php +++ b/lib/GaletteObjectsLend/Repository/Categories.php @@ -10,347 +10,153 @@ namespace GaletteObjectsLend\Repository; -use Analog\Analog; +use ArrayObject; use Galette\Core\Db; -use Laminas\Db\ResultSet\ResultSet; -use Laminas\Db\Sql\Expression; -use Laminas\Db\Sql\Predicate\Operator; -use GaletteObjectsLend\Filters\CategoriesList; -use GaletteObjectsLend\Filters\ObjectsList; +use Galette\Core\Login; +use Galette\Core\Preferences; use GaletteObjectsLend\Entity\LendCategory; use GaletteObjectsLend\Entity\LendObject; -use Galette\Core\Login; +use GaletteObjectsLend\Entity\Preferences as LendPreferences; +use GaletteObjectsLend\Filters\CategoriesList; +use GaletteObjectsLend\Filters\ObjectsList; +use Laminas\Db\ResultSet\ResultSet; +use Laminas\Db\Sql\Expression; +use Laminas\Db\Sql\Predicate\Expression as PredicateExpression; use Laminas\Db\Sql\Select; /** * Categories list * * @author Johan Cwiklinski + * + * @extends AbstractRepository */ -class Categories +class Categories extends AbstractRepository { public const string TABLE = LendCategory::TABLE; public const string PK = LendCategory::PK; + protected const string ALIAS = 'c'; - public const int ALL_CATEGORIES = 0; - public const int ACTIVE_CATEGORIES = 1; - public const int INACTIVE_CATEGORIES = 2; + public const int ALL_CATEGORIES = self::ALL; + public const int ACTIVE_CATEGORIES = self::ACTIVE; + public const int INACTIVE_CATEGORIES = self::INACTIVE; public const int FILTER_NAME = 0; public const int ORDERBY_NAME = 0; public const int ORDERBY_ACTIVITY = 1; - private CategoriesList $filters; - private ?int $count = null; - /** @var array */ - private array $errors = []; - - private Db $zdb; - private Login $login; + /** @var CategoriesList */ + protected \Galette\Core\Pagination $filters; /** * Default constructor * - * @param Db $zdb Database instance - * @param Login $login Logged in instance - * @param ?CategoriesList $filters Filtering + * @param Db $zdb Database instance + * @param Preferences $preferences Preferences instance + * @param Login $login Logged in instance + * @param ?CategoriesList $filters Filtering */ - public function __construct(Db $zdb, Login $login, ?CategoriesList $filters = null) + public function __construct(Db $zdb, Preferences $preferences, Login $login, ?CategoriesList $filters = null) { - $this->zdb = $zdb; - $this->login = $login; - - if ($filters === null) { - $this->filters = new CategoriesList(); - } else { - $this->filters = $filters; - } + parent::__construct($zdb, $preferences, $login, 'LendCategory', $filters ?? new CategoriesList()); } - /** * Get categories list * - * @param bool $as_cat return the results as an array of - * Categories object. - * @param ?array $fields field(s) name(s) to get. Should be a string or - * an array. If null, all fields will be - * returned - * @param bool $count true if we want to count members - * @param bool $limit true if we want records pagination + * @param bool $as_cat return the results as an array of Category object. + * @param bool $count true if we want to count rows + * @param bool $limit true if we want records pagination * - * @return LendCategory[]|ResultSet + * @return ($as_cat is true ? LendCategory[] : ResultSet) */ - public function getCategoriesList( - bool $as_cat = false, - ?array $fields = null, - bool $count = true, - bool $limit = true - ): array|ResultSet { - try { - $select = $this->buildSelect($fields, $count); - - //add limits to retrieve only relevant rows - if ($limit === true) { - $this->filters->setLimits($select); - } - - $rows = $this->zdb->execute($select); - $this->filters->query = $this->zdb->query_string; - - $categories = []; - if ($as_cat) { - foreach ($rows as $row) { - $categories[] = new LendCategory($this->zdb, $row); - } - } else { - $categories = $rows; - } - return $categories; - } catch (\Exception $e) { - Analog::log( - 'Cannot list categories | ' . $e->getMessage(), - Analog::WARNING - ); - throw $e; - } + public function getCategoriesList(bool $as_cat = false, bool $count = true, bool $limit = true): array|ResultSet + { + return $this->fetchList($as_cat, $count, $limit); } /** - * Get Categories list + * Get whole categories list * - * @param bool $as_cat return the results as an array of - * Category object. - * @param ?array $fields field(s) name(s) to get. If null, all fields will be returned + * @param bool $as_cat return the results as an array of Category object. * - * @return LendCategory[]|ResultSet + * @return ($as_cat is true ? LendCategory[] : ResultSet) */ - public function getList(bool $as_cat = false, ?array $fields = null): array|ResultSet + public function getList(bool $as_cat = false): array|ResultSet { - return $this->getCategoriesList( - $as_cat, - $fields, - false, - false - ); + return $this->fetchList($as_cat, false, false); } /** * Builds the SELECT statement - * - * @param ?array $fields fields list to retrieve - * @param bool $count true if we want to count members, defaults to false - * - * @return Select SELECT statement */ - private function buildSelect(?array $fields, bool $count = false): Select + protected function buildSelect(): Select { - try { - $fieldsList = [ - '*', - 'objects_count' => new Expression('COUNT(o.' . self::PK . ')'), - 'objects_price_sum' => new Expression('SUM(o.price)') - ]; - - if ($fields !== null && is_array($fields)) { - array_merge($fieldsList, $fields); - } - - $select = $this->zdb->select(LEND_PREFIX . self::TABLE, 'c'); - $select->columns($fieldsList); + $select = $this->zdb->select(LEND_PREFIX . self::TABLE, self::ALIAS); + $select->columns([ + '*', + 'objects_count' => new Expression('COUNT(o.' . LendObject::PK . ')'), + 'objects_price_sum' => new Expression('SUM(o.price)') + ]); + + $select->join( + ['o' => PREFIX_DB . LEND_PREFIX . LendObject::TABLE], + 'o.' . LendCategory::PK . '=c.' . LendCategory::PK, + [], + $select::JOIN_LEFT + ); - $select->join( - ['o' => PREFIX_DB . LEND_PREFIX . LendObject::TABLE], - 'o.' . LendCategory::PK . '=c.' . LendCategory::PK, - [], - $select::JOIN_LEFT + //categories of filtered objects only + if ($this->filters->objects_filters instanceof ObjectsList) { + $objects = new Objects( + $this->zdb, + $this->preferences, + $this->login, + new LendPreferences($this->zdb), + $this->filters->objects_filters ); + $objects->applyFilters($select); + } - if ($this->filters !== false) { - $this->buildWhereClause($select); - } - $select->order($this->buildOrderClause($fields)); - - if ($count) { - $this->proceedCount($select); - } - - $select->group( - 'c.category_id' - ); + $this->whereActive($select, $this->filters->active_filter); - return $select; - } catch (\Exception $e) { - Analog::log( - 'Cannot build SELECT clause for categories | ' . $e->getMessage(), - Analog::WARNING - ); - throw $e; + if ((string)$this->filters->filter_str !== '') { + $select->where($this->contains('c.name', $this->filters->filter_str)); } - } - - /** - * Count categories from the query - * - * @param Select $select Original select - */ - private function proceedCount(Select $select): void - { - try { - $countSelect = clone $select; - $countSelect->reset($countSelect::COLUMNS); - $countSelect->reset($countSelect::ORDER); - $countSelect->reset($countSelect::JOINS); - $countSelect->columns( - [ - 'count' => new Expression('count(DISTINCT c.' . self::PK . ')') - ] - ); - $joins = $select->getRawState($select::JOINS); - foreach ($joins as $join) { - $countSelect->join( - $join['name'], - $join['on'], - [], - $join['type'] - ); - } + $select->group('c.' . self::PK); - $results = $this->zdb->execute($countSelect); - $this->count = 0; - if ($results->count() > 0) { - $this->count = (int)$results->current()->count; - } - $this->filters->setCounter($this->count); - } catch (\Exception $e) { - Analog::log( - 'Cannot count categories | ' . $e->getMessage(), - Analog::WARNING - ); - throw $e; + if ($this->filters->not_empty === true) { + $select->having(new PredicateExpression('COUNT(o.' . LendObject::PK . ') > 0')); } + + return $select; } /** * Builds the order clause * - * @param ?string[] $fields Fields list to ensure ORDER clause - * references selected fields. Optional. - * * @return array SQL ORDER clauses */ - private function buildOrderClause(?array $fields = null): array - { - $order = []; - switch ($this->filters->orderby) { - case self::ORDERBY_NAME: - if ($this->canOrderBy('name', $fields)) { - $order[] = 'name ' . $this->filters->getDirection(); - } - break; - case self::ORDERBY_ACTIVITY: - if ($this->canOrderBy('is_active', $fields)) { - $order[] = 'is_active ' . $this->filters->getDirection(); - } - break; - } - - return $order; - } - - /** - * Builds where clause, for filtering on simple list mode - * - * @param Select $select Original select - */ - private function buildWhereClause(Select $select): void - { - try { - //if there are filters on objects; add them - if ($this->filters->objects_filters instanceof ObjectsList) { - $objects = new Objects( - $this->zdb, - new \GaletteObjectsLend\Entity\Preferences($this->zdb), - $this->filters->objects_filters - ); - $objects->buildWhereClause($select); - } - - if ($this->filters->active_filter == self::ACTIVE_CATEGORIES) { - $select->where('c.is_active = true'); - } - if ($this->filters->active_filter == self::INACTIVE_CATEGORIES) { - $select->where('c.is_active = false'); - } - - if ($this->filters->filter_str != '') { - $token = $this->zdb->platform->quoteValue( - '%' . strtolower($this->filters->filter_str) . '%' - ); - - $select->where( - 'LOWER(c.name) LIKE ' . $token - ); - } - - if ($this->filters->not_empty == true) { - $select->having( - new Operator( - new Expression('COUNT(o.' . self::PK . ')'), - '>', - '0' - ) - ); - } - } catch (\Exception $e) { - Analog::log( - __METHOD__ . ' | ' . $e->getMessage(), - Analog::WARNING - ); - throw $e; - } - } - - /** - * Is field allowed to order? it should be present in - * provided fields list (those that are SELECT'ed). - * - * @param string $field_name Field name to order by - * @param ?array $fields SELECTE'ed fields - */ - private function canOrderBy(string $field_name, ?array $fields): bool + protected function buildOrderClause(): array { - if (!is_array($fields)) { - return true; - } elseif (in_array($field_name, $fields)) { - return true; - } else { - Analog::log( - 'Trying to order by ' . $field_name . ' while it is not in ' - . 'selected fields.', - Analog::WARNING - ); - return false; - } - } + $field = match ($this->filters->orderby) { + self::ORDERBY_NAME => 'c.name', + self::ORDERBY_ACTIVITY => 'c.is_active', + default => null + }; - /** - * Get count for current query - */ - public function getCount(): ?int - { - return $this->count; + return $field === null ? [] : [$field . ' ' . $this->filters->getDirection()]; } /** - * Get registered errors + * Create an entity from a resultset row * - * @return array + * @param ArrayObject $row Resultset row */ - public function getErrors(): array + protected function createEntity(ArrayObject $row): LendCategory { - return $this->errors; + return new LendCategory($this->zdb, $row); } } diff --git a/lib/GaletteObjectsLend/Repository/Objects.php b/lib/GaletteObjectsLend/Repository/Objects.php index 683f4b34..cbf14567 100644 --- a/lib/GaletteObjectsLend/Repository/Objects.php +++ b/lib/GaletteObjectsLend/Repository/Objects.php @@ -11,31 +11,37 @@ namespace GaletteObjectsLend\Repository; use Analog\Analog; +use ArrayObject; use Galette\Core\Db; -use Laminas\Db\ResultSet\ResultSet; -use Laminas\Db\Sql\Expression; +use Galette\Core\Login; +use Galette\Core\Preferences; use Galette\Entity\Adherent; -use GaletteObjectsLend\Filters\ObjectsList; -use GaletteObjectsLend\Entity\Preferences; -use GaletteObjectsLend\Entity\LendObject; use GaletteObjectsLend\Entity\LendCategory; +use GaletteObjectsLend\Entity\LendObject; use GaletteObjectsLend\Entity\LendRent; use GaletteObjectsLend\Entity\LendStatus; +use GaletteObjectsLend\Entity\Preferences as LendPreferences; +use GaletteObjectsLend\Filters\ObjectsList; +use Laminas\Db\ResultSet\ResultSet; use Laminas\Db\Sql\Select; /** * Objects list * + * @author Mélissa Djebel * @author Johan Cwiklinski + * + * @extends AbstractRepository */ -class Objects +class Objects extends AbstractRepository { public const string TABLE = LendObject::TABLE; public const string PK = LendObject::PK; + protected const string ALIAS = 'o'; - public const int ALL_OBJECTS = 0; - public const int ACTIVE_OBJECTS = 1; - public const int INACTIVE_OBJECTS = 2; + public const int ALL_OBJECTS = self::ALL; + public const int ACTIVE_OBJECTS = self::ACTIVE; + public const int INACTIVE_OBJECTS = self::INACTIVE; public const int FILTER_NAME = 0; public const int FILTER_SERIAL = 1; @@ -53,77 +59,52 @@ class Objects public const int ORDERBY_MEMBER = 8; public const int ORDERBY_CATEGORY = 9; - private Db $zdb; - - private ObjectsList $filters; - private ?int $count = null; - /** @var array */ - private array $errors = []; - private Preferences $prefs; + /** @var ObjectsList */ + protected \Galette\Core\Pagination $filters; /** * Default constructor * - * @param Db $zdb Database instance - * @param Preferences $lprefs Lends preferences instance - * @param ?ObjectsList $filters Filtering + * @param Db $zdb Database instance + * @param Preferences $preferences Preferences instance + * @param Login $login Logged in instance + * @param LendPreferences $lendsprefs Lends preferences instance + * @param ?ObjectsList $filters Filtering */ - public function __construct(Db $zdb, Preferences $lprefs, ?ObjectsList $filters = null) - { - $this->zdb = $zdb; - $this->prefs = $lprefs; - - if ($filters === null) { - $this->filters = new ObjectsList(); - } else { - $this->filters = $filters; - } + public function __construct( + Db $zdb, + Preferences $preferences, + Login $login, + private LendPreferences $lendsprefs, + ?ObjectsList $filters = null + ) { + parent::__construct($zdb, $preferences, $login, 'LendObject', $filters ?? new ObjectsList()); } /** * Get objects list * - * @param bool $as_objects return the results as an array of - * Object object. - * @param ?array $fields field(s) name(s) to get. If null, all fields will be returned - * @param bool $count true if we want to count members - * @param bool $limit true if we want records pagination + * @param bool $as_objects return the results as an array of Object object. + * @param bool $count true if we want to count rows + * @param bool $limit true if we want records pagination * - * @return LendObject[]|ResultSet + * @return ($as_objects is true ? LendObject[] : ResultSet) */ - public function getObjectsList( - bool $as_objects = false, - ?array $fields = null, - bool $count = true, - bool $limit = true - ): array|ResultSet { - try { - $select = $this->buildSelect($fields, $count); - - //add limits to retrieve only relevant rows - if ($limit === true) { - $this->filters->setLimits($select); - } - - $rows = $this->zdb->execute($select); - $this->filters->query = $this->zdb->query_string; + public function getObjectsList(bool $as_objects = false, bool $count = true, bool $limit = true): array|ResultSet + { + return $this->fetchList($as_objects, $count, $limit); + } - $objects = []; - if ($as_objects) { - foreach ($rows as $row) { - $objects[] = new LendObject($this->zdb, $row); - } - } else { - $objects = $rows; - } - return $objects; - } catch (\Exception $e) { - Analog::log( - 'Cannot list objects | ' . $e->getMessage(), - Analog::WARNING - ); - throw $e; - } + /** + * Get whole objects list + * + * @param bool $as_objects return the results as an array of Object object. + * + * @return ($as_objects is true ? LendObject[] : ResultSet) + */ + public function getList(bool $as_objects = false): array|ResultSet + { + return $this->fetchList($as_objects, false, false); } /** @@ -189,25 +170,6 @@ public function removeObjects(array $ids): bool return false; } - /** - * Get Objects list - * - * @param bool $as_objects return the results as an array of - * Object object. - * @param ?array $fields field(s) name(s) to get. If null, all fields will be returned - * - * @return LendObject[]|ResultSet - */ - public function getList(bool $as_objects = false, ?array $fields = null): array|ResultSet - { - return $this->getObjectsList( - $as_objects, - $fields, - false, - false - ); - } - /** * Builds the SELECT statement for objects with their current rent and their category */ @@ -264,281 +226,112 @@ public function getWithCurrentRent(int $id): LendObject /** * Builds the SELECT statement - * - * @param ?string[] $fields fields list to retrieve - * @param bool $count true if we want to count members, defaults to false - * - * @return Select SELECT statement */ - private function buildSelect(?array $fields, bool $count = false): Select + protected function buildSelect(): Select { - try { - $select = $this->buildBaseSelect(); - - $fieldsList = ($fields != null) - ? ((!is_array($fields) || count($fields) < 1) ? (array)'*' - : $fields) : (array)'*'; - - $select->columns($fieldsList); - - if ($this->filters !== false) { - $this->buildWhereClause($select); - } - - $select->order($this->buildOrderClause($fields)); - - if ($count) { - $this->proceedCount($select); - } - - return $select; - } catch (\Exception $e) { - Analog::log( - 'Cannot build SELECT clause for objects | ' . $e->getMessage(), - Analog::WARNING - ); - throw $e; - } + $select = $this->buildBaseSelect(); + $this->applyFilters($select); + return $select; } /** - * Count members from the query + * Builds the order clause * - * @param Select $select Original select + * @return array SQL ORDER clauses */ - private function proceedCount(Select $select): void + protected function buildOrderClause(): array { - try { - $countSelect = clone $select; - $countSelect->reset($countSelect::COLUMNS); - $countSelect->reset($countSelect::ORDER); - $countSelect->reset($countSelect::HAVING); - $countSelect->reset($countSelect::JOINS); - $countSelect->columns( - [ - 'count' => new Expression('count(o.' . self::PK . ')') - ] - ); - - $have = $select->having; - if ($have->count() > 0) { - foreach ($have->getPredicates() as $h) { - $countSelect->where($h); - } - } - - $joins = $select->getRawState($select::JOINS); - foreach ($joins as $join) { - $countSelect->join( - $join['name'], - $join['on'], - [], - $join['type'] - ); - } - - $results = $this->zdb->execute($countSelect); - - $this->count = (int)$results->current()->count; - if ($this->count > 0) { - $this->filters->setCounter($this->count); - } - } catch (\Exception $e) { - Analog::log( - 'Cannot count objects | ' . $e->getMessage(), - Analog::WARNING - ); - throw $e; - } + $fields = match ($this->filters->orderby) { + self::ORDERBY_NAME => ['o.name'], + self::ORDERBY_SERIAL => ['o.serial_number'], + self::ORDERBY_PRICE => ['o.price'], + self::ORDERBY_RENTPRICE => ['o.rent_price'], + self::ORDERBY_WEIGHT => ['o.weight'], + self::ORDERBY_STATUS => ['s.status_text'], + self::ORDERBY_BDATE => ['r.date_begin'], + self::ORDERBY_FDATE => ['r.date_forecast'], + self::ORDERBY_MEMBER => ['a.nom_adh', 'a.prenom_adh'], + self::ORDERBY_CATEGORY => ['c.name'], + default => [] + }; + + $direction = $this->filters->getDirection(); + return array_map(fn(string $field) => $field . ' ' . $direction, $fields); } /** - * Builds the order clause + * Create an entity from a resultset row * - * @param array $fields Fields list to ensure ORDER clause - * references selected fields. Optional. - * - * @return array SQL ORDER clauses + * @param ArrayObject $row Resultset row */ - private function buildOrderClause(?array $fields = null): array + protected function createEntity(ArrayObject $row): LendObject { - $order = []; - switch ($this->filters->orderby) { - case self::ORDERBY_NAME: - if ($this->canOrderBy('name', $fields)) { - $order[] = 'o.name ' . $this->filters->getDirection(); - } - break; - case self::ORDERBY_SERIAL: - if ($this->canOrderBy('serial_number', $fields)) { - $order[] = 'o.serial_number ' . $this->filters->getDirection(); - } - break; - case self::ORDERBY_PRICE: - if ($this->canOrderBy('price', $fields)) { - $order[] = 'o.price ' . $this->filters->getDirection(); - } - break; - case self::ORDERBY_RENTPRICE: - if ($this->canOrderBy('rent_price', $fields)) { - $order[] = 'o.rent_price ' . $this->filters->getDirection(); - } - break; - case self::ORDERBY_WEIGHT: - if ($this->canOrderBy('weight', $fields)) { - $order[] = 'o.weight ' . $this->filters->getDirection(); - } - break; - case self::ORDERBY_STATUS: - if ($this->canOrderBy('status_text', $fields)) { - $order[] = 's.status_text ' . $this->filters->getDirection(); - } - break; - case self::ORDERBY_BDATE: - if ($this->canOrderBy('date_begin', $fields)) { - $order[] = 'r.date_begin ' . $this->filters->getDirection(); - } - break; - case self::ORDERBY_FDATE: - if ($this->canOrderBy('date_forecast', $fields)) { - $order[] = 'r.date_forecast ' . $this->filters->getDirection(); - } - break; - case self::ORDERBY_MEMBER: - if ($this->canOrderBy('nom_adh', $fields) && $this->canOrderBy('prenom_adh', $fields)) { - $order[] = 'a.nom_adh ' . $this->filters->getDirection() - . ', a.prenom_adh ' . $this->filters->getDirection(); - } - break; - case self::ORDERBY_CATEGORY: - if ($this->canOrderBy('cat_name', $fields)) { - $order[] = 'c.name ' . $this->filters->getDirection(); - } - break; - } - - return $order; + return new LendObject($this->zdb, $row); } /** - * Builds where clause, for filtering on simple list mode + * Add filters on objects * - * @param Select $select Original select + * Objects table must be aliased as "o", and categories one as "c". + * + * @param Select $select Select */ - public function buildWhereClause(Select $select): void + public function applyFilters(Select $select): void { - try { - if (is_array($this->filters->selected) && count($this->filters->selected) > 0) { - $select->where->in('o.' . self::PK, $this->filters->selected); - } - - if ($this->filters->active_filter == self::ACTIVE_OBJECTS) { - $select->where('(o.is_active = true AND (c.is_active IS NULL OR c.is_active = true))'); - } - if ($this->filters->active_filter == self::INACTIVE_OBJECTS) { - $select->where('(o.is_active = false OR (c.is_active IS NOT NULL AND c.is_active = false))'); - } - - if ($this->filters->category_filter == '-1') { - $select->where('o.' . LendCategory::PK . ' IS NULL'); - } elseif ($this->filters->category_filter !== null) { - $select->where('o.' . LendCategory::PK . '=' . $this->filters->category_filter); - } - - if ($this->filters->filter_str != '') { - $token = $this->zdb->platform->quoteValue( - '%' . strtolower($this->filters->filter_str) . '%' - ); + if (count($this->filters->selected) > 0) { + $select->where->in('o.' . self::PK, $this->filters->selected); + } - switch ($this->filters->field_filter) { - case self::FILTER_NAME: - if (TYPE_DB === 'pgsql') { - $sep = " || ' ' || "; - $pre = ''; - $post = ''; - } else { - $sep = ', " ", '; - $pre = 'CONCAT('; - $post = ')'; - } - - if ($this->prefs->getPreferences()['VIEW_DESCRIPTION']) { - $select->where( - '(' - . $pre . 'LOWER(o.name)' . $sep - . 'LOWER(o.description)' . $post . ' LIKE ' - . $token . ')' - ); - } else { - $select->where( - 'LOWER(o.name) LIKE ' . $token - ); - } - break; - case self::FILTER_SERIAL: - $select->where( - 'LOWER(o.serial_number) LIKE ' - . $token - ); - break; - case self::FILTER_DIM: - $select->where( - 'LOWER(o.dimension) LIKE ' - . $token - ); - break; - case self::FILTER_ID: - $select->where->equalTo('o.' . LendObject::PK, $this->filters->filter_str); - break; - } - } - } catch (\Exception $e) { - Analog::log( - __METHOD__ . ' | ' . $e->getMessage(), - Analog::WARNING - ); - throw $e; + //object and its category, if any, must be active + if ($this->filters->active_filter === self::ACTIVE) { + $select->where + ->equalTo('o.is_active', 1) + ->nest() + ->isNull('c.is_active') + ->or + ->equalTo('c.is_active', 1) + ->unnest(); + } elseif ($this->filters->active_filter === self::INACTIVE) { + $select->where + ->nest() + ->equalTo('o.is_active', 0) + ->or + ->equalTo('c.is_active', 0) + ->unnest(); } - } - /** - * Is field allowed to order? it should be present in - * provided fields list (those that are SELECT'ed). - * - * @param string $field_name Field name to order by - * @param ?string[] $fields SELECTE'ed fields - */ - private function canOrderBy(string $field_name, ?array $fields): bool - { - if (!is_array($fields)) { - return true; - } elseif (in_array($field_name, $fields)) { - return true; - } else { - Analog::log( - 'Trying to order by ' . $field_name . ' while it is not in ' - . 'selected fields.', - Analog::WARNING - ); - return false; + if ($this->filters->category_filter === -1) { + $select->where->isNull('o.' . LendCategory::PK); + } elseif ($this->filters->category_filter !== null) { + $select->where->equalTo('o.' . LendCategory::PK, $this->filters->category_filter); } - } - /** - * Get count for current query - */ - public function getCount(): ?int - { - return $this->count; - } + $search = (string)$this->filters->filter_str; + if ($search === '') { + return; + } - /** - * Get registered errors - * - * @return array - */ - public function getErrors(): array - { - return $this->errors; + switch ($this->filters->field_filter) { + case self::FILTER_NAME: + if ($this->lendsprefs->{LendPreferences::PARAM_VIEW_DESCRIPTION}) { + $select->where + ->nest() + ->addPredicate($this->contains('o.name', $search)) + ->orPredicate($this->contains('o.description', $search)) + ->unnest(); + } else { + $select->where($this->contains('o.name', $search)); + } + break; + case self::FILTER_SERIAL: + $select->where($this->contains('o.serial_number', $search)); + break; + case self::FILTER_DIM: + $select->where($this->contains('o.dimension', $search)); + break; + case self::FILTER_ID: + $select->where->equalTo('o.' . self::PK, (int)$search); + break; + } } } diff --git a/lib/GaletteObjectsLend/Repository/Status.php b/lib/GaletteObjectsLend/Repository/Status.php index 06a3b750..82843c2c 100644 --- a/lib/GaletteObjectsLend/Repository/Status.php +++ b/lib/GaletteObjectsLend/Repository/Status.php @@ -10,28 +10,28 @@ namespace GaletteObjectsLend\Repository; -use Analog\Analog; +use ArrayObject; use Galette\Core\Db; -use Laminas\Db\ResultSet\ResultSet; -use Laminas\Db\Sql\Expression; -use GaletteObjectsLend\Filters\StatusList; -use GaletteObjectsLend\Entity\LendStatus; use Galette\Core\Login; +use Galette\Core\Preferences; +use GaletteObjectsLend\Entity\LendStatus; +use GaletteObjectsLend\Filters\StatusList; +use Laminas\Db\ResultSet\ResultSet; use Laminas\Db\Sql\Select; /** - * Status list + * Lend status list * + * @author Mélissa Djebel * @author Johan Cwiklinski + * + * @extends AbstractRepository */ -class Status +class Status extends AbstractRepository { public const string TABLE = LendStatus::TABLE; public const string PK = LendStatus::PK; - - public const int ALL = 0; - public const int ACTIVE = 1; - public const int INACTIVE = 2; + protected const string ALIAS = 'c'; public const int DC_STOCK = 0; public const int IN_STOCK = 1; @@ -45,300 +45,97 @@ class Status public const int ORDERBY_STOCK = 3; public const int ORDERBY_RENTDAYS = 4; - private Db $zdb; - private Login $login; - - private StatusList $filters; - private ?int $count = null; - /** @var array */ - private array $errors = []; + /** @var StatusList */ + protected \Galette\Core\Pagination $filters; /** * Default constructor * - * @param Db $zdb Database instance - * @param Login $login Logged in instance - * @param ?StatusList $filters Filtering + * @param Db $zdb Database instance + * @param Preferences $preferences Preferences instance + * @param Login $login Logged in instance + * @param ?StatusList $filters Filtering */ - public function __construct(Db $zdb, Login $login, ?StatusList $filters = null) + public function __construct(Db $zdb, Preferences $preferences, Login $login, ?StatusList $filters = null) { - $this->zdb = $zdb; - $this->login = $login; - - if ($filters === null) { - $this->filters = new StatusList(); - } else { - $this->filters = $filters; - } + parent::__construct($zdb, $preferences, $login, 'LendStatus', $filters ?? new StatusList()); } - /** * Get status list * - * @param bool $as_stt return the results as an array of - * Status object. - * @param ?string[] $fields field(s) name(s) to get. If null, all fields will be returned - * @param bool $count true if we want to count members - * @param bool $limit true if we want records pagination + * @param bool $as_stt return the results as an array of Status object. + * @param bool $count true if we want to count rows + * @param bool $limit true if we want records pagination * - * @return LendStatus[]|ResultSet + * @return ($as_stt is true ? LendStatus[] : ResultSet) */ - public function getStatusList( - bool $as_stt = false, - ?array $fields = null, - bool $count = true, - bool $limit = true - ): array|ResultSet { - try { - $select = $this->buildSelect($fields, false, $count); - - //add limits to retrieve only relevant rows - if ($limit === true) { - $this->filters->setLimits($select); - } - - $rows = $this->zdb->execute($select); - $this->filters->query = $this->zdb->query_string; - - $status = []; - if ($as_stt) { - foreach ($rows as $row) { - $status[] = new LendStatus($this->zdb, $row); - } - } else { - $status = $rows; - } - return $status; - } catch (\Exception $e) { - Analog::log( - 'Cannot list categories | ' . $e->getMessage(), - Analog::WARNING - ); - throw $e; - } + public function getStatusList(bool $as_stt = false, bool $count = true, bool $limit = true): array|ResultSet + { + return $this->fetchList($as_stt, $count, $limit); } /** - * Get status list + * Get whole status list * - * @param bool $as_stt return the results as an array of - * Status object. - * @param ?string[] $fields field(s) name(s) to get. Should be a string or - * an array. If null, all fields will be - * returned + * @param bool $as_stt return the results as an array of Status object. * - * @return LendStatus[]|ResultSet + * @return ($as_stt is true ? LendStatus[] : ResultSet) */ - public function getList(bool $as_stt = false, ?array $fields = null): array|ResultSet + public function getList(bool $as_stt = false): array|ResultSet { - return $this->getStatusList( - $as_stt, - $fields, - false, - false - ); + return $this->fetchList($as_stt, false, false); } /** * Builds the SELECT statement - * - * @param string[] $fields fields list to retrieve - * @param bool $photos true if we want to get only members with photos - * Default to false, only relevant for SHOW_PUBLIC_LIST - * @param bool $count true if we want to count members, defaults to false - * - * @return Select SELECT statement */ - private function buildSelect(?array $fields, bool $photos = false, bool $count = false): Select + protected function buildSelect(): Select { - try { - $fieldsList = ($fields != null) - ? ((!is_array($fields) || count($fields) < 1) ? (array)'*' - : $fields) : (array)'*'; - - $select = $this->zdb->select(LEND_PREFIX . self::TABLE, 'c'); - $select->columns($fieldsList); + $select = $this->zdb->select(LEND_PREFIX . self::TABLE, self::ALIAS); - if ($this->filters !== false) { - $this->buildWhereClause($select); - } - $select->order($this->buildOrderClause($fields)); + $this->whereActive($select, $this->filters->active_filter); - if ($count) { - $this->proceedCount($select); - } - - return $select; - } catch (\Exception $e) { - Analog::log( - 'Cannot build SELECT clause for objectslend status | ' . $e->getMessage(), - Analog::WARNING - ); - throw $e; + if ($this->filters->stock_filter === self::IN_STOCK) { + $select->where(['c.in_stock' => 1]); + } elseif ($this->filters->stock_filter === self::OUT_STOCK) { + $select->where(['c.in_stock' => 0]); } - } - - /** - * Count members from the query - * - * @param Select $select Original select - */ - private function proceedCount(Select $select): void - { - try { - $countSelect = clone $select; - $countSelect->reset($countSelect::COLUMNS); - $countSelect->reset($countSelect::ORDER); - $countSelect->reset($countSelect::HAVING); - $countSelect->columns( - [ - 'count' => new Expression('count(c.' . self::PK . ')') - ] - ); - $have = $select->having; - if ($have->count() > 0) { - foreach ($have->getPredicates() as $h) { - $countSelect->where($h); - } - } - - $results = $this->zdb->execute($countSelect); - - $this->count = (int)$results->current()->count; - if ($this->count > 0) { - $this->filters->setCounter($this->count); - } - } catch (\Exception $e) { - Analog::log( - 'Cannot count objectslend status | ' . $e->getMessage(), - Analog::WARNING - ); - throw $e; + if ((string)$this->filters->filter_str !== '') { + $select->where($this->contains('c.status_text', $this->filters->filter_str)); } + + return $select; } /** * Builds the order clause * - * @param ?string[] $fields Fields list to ensure ORDER clause - * references selected fields. Optional. - * * @return array SQL ORDER clauses */ - private function buildOrderClause(?array $fields = null): array - { - $order = []; - switch ($this->filters->orderby) { - case self::ORDERBY_ID: - if ($this->canOrderBy('status_id', $fields)) { - $order[] = 'status_id ' . $this->filters->getDirection(); - } - break; - case self::ORDERBY_NAME: - if ($this->canOrderBy('status_text', $fields)) { - $order[] = 'status_text ' . $this->filters->getDirection(); - } - break; - case self::ORDERBY_ACTIVE: - if ($this->canOrderBy('is_active', $fields)) { - $order[] = 'is_active ' . $this->filters->getDirection(); - } - break; - case self::ORDERBY_STOCK: - if ($this->canOrderBy('in_stock', $fields)) { - $order[] = 'in_stock ' . $this->filters->getDirection(); - } - break; - case self::ORDERBY_RENTDAYS: - if ($this->canOrderBy('rent_day_number', $fields)) { - $order[] = 'rent_day_number ' . $this->filters->getDirection(); - } - break; - } - - return $order; - } - - /** - * Builds where clause, for filtering on simple list mode - * - * @param Select $select Original select - */ - private function buildWhereClause(Select $select): void - { - try { - if ($this->filters->active_filter == self::ACTIVE) { - $select->where('c.is_active = true'); - } - if ($this->filters->active_filter == self::INACTIVE) { - $select->where('c.is_active = false'); - } - - if ($this->filters->stock_filter == self::IN_STOCK) { - $select->where('c.in_stock = true'); - } - if ($this->filters->stock_filter == self::OUT_STOCK) { - $select->where('c.in_stock = false'); - } - - if ($this->filters->filter_str != '') { - $token = $this->zdb->platform->quoteValue( - '%' . strtolower($this->filters->filter_str) . '%' - ); - - $select->where( - 'LOWER(c.status_text) LIKE ' . $token - ); - } - } catch (\Exception $e) { - Analog::log( - __METHOD__ . ' | ' . $e->getMessage(), - Analog::WARNING - ); - } - } - - /** - * Is field allowed to order? it should be present in - * provided fields list (those that are SELECT'ed). - * - * @param string $field_name Field name to order by - * @param ?string[] $fields SELECTE'ed fields - */ - private function canOrderBy(string $field_name, ?array $fields): bool - { - if (!is_array($fields)) { - return true; - } elseif (in_array($field_name, $fields)) { - return true; - } else { - Analog::log( - 'Trying to order by ' . $field_name . ' while it is not in ' - . 'selected fields.', - Analog::WARNING - ); - return false; - } - } - - /** - * Get count for current query - */ - public function getCount(): ?int + protected function buildOrderClause(): array { - return $this->count; + $field = match ($this->filters->orderby) { + self::ORDERBY_ID => 'status_id', + self::ORDERBY_NAME => 'status_text', + self::ORDERBY_ACTIVE => 'is_active', + self::ORDERBY_STOCK => 'in_stock', + self::ORDERBY_RENTDAYS => 'rent_day_number', + default => null + }; + + return $field === null ? [] : [$field . ' ' . $this->filters->getDirection()]; } /** - * Get registered errors + * Create an entity from a resultset row * - * @return array + * @param ArrayObject $row Resultset row */ - public function getErrors(): array + protected function createEntity(ArrayObject $row): LendStatus { - return $this->errors; + return new LendStatus($this->zdb, $row); } /** diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 8d425e62..823140cb 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -65,105 +65,3 @@ parameters: identifier: property.onlyWritten count: 1 path: lib/GaletteObjectsLend/IO/PdfObjects.php - - - - message: '#^Call to function array_merge\(\) on a separate line has no effect\.$#' - identifier: function.resultUnused - count: 1 - path: lib/GaletteObjectsLend/Repository/Categories.php - - - - message: '#^Call to function is_array\(\) with array\ will always evaluate to true\.$#' - identifier: function.alreadyNarrowedType - count: 1 - path: lib/GaletteObjectsLend/Repository/Categories.php - - - - message: '#^Method GaletteObjectsLend\\Repository\\Categories\:\:getList\(\) has parameter \$fields with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: lib/GaletteObjectsLend/Repository/Categories.php - - - - message: '#^Parameter \#1 \$left of class Laminas\\Db\\Sql\\Predicate\\Operator constructor expects bool\|float\|int\|string\|null, Laminas\\Db\\Sql\\Expression given\.$#' - identifier: argument.type - count: 1 - path: lib/GaletteObjectsLend/Repository/Categories.php - - - - message: '#^Property GaletteObjectsLend\\Repository\\Categories\:\:\$login is never read, only written\.$#' - identifier: property.onlyWritten - count: 1 - path: lib/GaletteObjectsLend/Repository/Categories.php - - - - message: '#^Strict comparison using \!\=\= between GaletteObjectsLend\\Filters\\CategoriesList and false will always evaluate to true\.$#' - identifier: notIdentical.alwaysTrue - count: 1 - path: lib/GaletteObjectsLend/Repository/Categories.php - - - - message: '#^Call to function is_array\(\) with array will always evaluate to true\.$#' - identifier: function.alreadyNarrowedType - count: 1 - path: lib/GaletteObjectsLend/Repository/Objects.php - - - - message: '#^Call to function is_array\(\) with non\-empty\-array\ will always evaluate to true\.$#' - identifier: function.alreadyNarrowedType - count: 1 - path: lib/GaletteObjectsLend/Repository/Objects.php - - - - message: '#^Comparison operation "\<" between int\<1, max\> and 1 is always false\.$#' - identifier: smaller.alwaysFalse - count: 1 - path: lib/GaletteObjectsLend/Repository/Objects.php - - - - message: '#^Result of \|\| is always false\.$#' - identifier: booleanOr.alwaysFalse - count: 1 - path: lib/GaletteObjectsLend/Repository/Objects.php - - - - message: '#^Strict comparison using \!\=\= between GaletteObjectsLend\\Filters\\ObjectsList and false will always evaluate to true\.$#' - identifier: notIdentical.alwaysTrue - count: 1 - path: lib/GaletteObjectsLend/Repository/Objects.php - - - - message: '#^Strict comparison using \=\=\= between ''pgsql'' and ''pgsql'' will always evaluate to true\.$#' - identifier: identical.alwaysTrue - count: 1 - path: lib/GaletteObjectsLend/Repository/Objects.php - - - - message: '#^Call to function is_array\(\) with non\-empty\-array\ will always evaluate to true\.$#' - identifier: function.alreadyNarrowedType - count: 1 - path: lib/GaletteObjectsLend/Repository/Status.php - - - - message: '#^Comparison operation "\<" between int\<1, max\> and 1 is always false\.$#' - identifier: smaller.alwaysFalse - count: 1 - path: lib/GaletteObjectsLend/Repository/Status.php - - - - message: '#^Property GaletteObjectsLend\\Repository\\Status\:\:\$login is never read, only written\.$#' - identifier: property.onlyWritten - count: 1 - path: lib/GaletteObjectsLend/Repository/Status.php - - - - message: '#^Result of \|\| is always false\.$#' - identifier: booleanOr.alwaysFalse - count: 1 - path: lib/GaletteObjectsLend/Repository/Status.php - - - - message: '#^Strict comparison using \!\=\= between GaletteObjectsLend\\Filters\\StatusList and false will always evaluate to true\.$#' - identifier: notIdentical.alwaysTrue - count: 1 - path: lib/GaletteObjectsLend/Repository/Status.php diff --git a/tests/GaletteObjectsLend/Controllers/tests/units/ObjectsController.php b/tests/GaletteObjectsLend/Controllers/tests/units/ObjectsController.php index bcb4e5eb..23cdb6d8 100644 --- a/tests/GaletteObjectsLend/Controllers/tests/units/ObjectsController.php +++ b/tests/GaletteObjectsLend/Controllers/tests/units/ObjectsController.php @@ -465,7 +465,7 @@ public function testFilterOnField(): void $filters = $this->session->objectslend_filter_objects; $this->assertSame(\GaletteObjectsLend\Repository\Objects::FILTER_SERIAL, $filters->field_filter); - $objects = new \GaletteObjectsLend\Repository\Objects($this->zdb, new Preferences($this->zdb), $filters); + $objects = new \GaletteObjectsLend\Repository\Objects($this->zdb, $this->preferences, $this->login, new Preferences($this->zdb), $filters); $list = $objects->getObjectsList(true); $this->assertCount(1, $list); } diff --git a/tests/GaletteObjectsLend/Entity/tests/units/LendObject.php b/tests/GaletteObjectsLend/Entity/tests/units/LendObject.php index 9b3609b8..67d3018e 100644 --- a/tests/GaletteObjectsLend/Entity/tests/units/LendObject.php +++ b/tests/GaletteObjectsLend/Entity/tests/units/LendObject.php @@ -91,6 +91,8 @@ public function testCrud(): void { $objects = new \GaletteObjectsLend\Repository\Objects( $this->zdb, + $this->preferences, + $this->login, new \GaletteObjectsLend\Entity\Preferences($this->zdb) ); $object = new \GaletteObjectsLend\Entity\LendObject($this->zdb); diff --git a/tests/GaletteObjectsLend/Entity/tests/units/LendRent.php b/tests/GaletteObjectsLend/Entity/tests/units/LendRent.php index 326c94db..d0567d9f 100644 --- a/tests/GaletteObjectsLend/Entity/tests/units/LendRent.php +++ b/tests/GaletteObjectsLend/Entity/tests/units/LendRent.php @@ -163,14 +163,14 @@ public function testCrud(): void $this->assertTrue($status->store()); $status_three = $status->getId(); - $list = (new \GaletteObjectsLend\Repository\Status($this->zdb, $this->login))->getActiveTakeAwayStatuses(); + $list = (new \GaletteObjectsLend\Repository\Status($this->zdb, $this->preferences, $this->login))->getActiveTakeAwayStatuses(); $this->assertCount(1, $list); $active_one = $list[0]; $this->assertSame($status_two, $active_one->getId()); $this->assertSame('Another active status', $active_one->getText()); - $list = (new \GaletteObjectsLend\Repository\Status($this->zdb, $this->login))->getActiveStockStatuses(); + $list = (new \GaletteObjectsLend\Repository\Status($this->zdb, $this->preferences, $this->login))->getActiveStockStatuses(); $this->assertCount(1, $list); $active_one = $list[0]; diff --git a/tests/GaletteObjectsLend/Entity/tests/units/LendStatus.php b/tests/GaletteObjectsLend/Entity/tests/units/LendStatus.php index 80a83dd2..f1874a55 100644 --- a/tests/GaletteObjectsLend/Entity/tests/units/LendStatus.php +++ b/tests/GaletteObjectsLend/Entity/tests/units/LendStatus.php @@ -80,14 +80,14 @@ public function testCrud(): void $status->setActive(false); $this->assertTrue($status->store()); - $list = (new \GaletteObjectsLend\Repository\Status($this->zdb, $this->login))->getActiveTakeAwayStatuses(); + $list = (new \GaletteObjectsLend\Repository\Status($this->zdb, $this->preferences, $this->login))->getActiveTakeAwayStatuses(); $this->assertCount(1, $list); $active_one = $list[0]; $this->assertSame($status_two, $active_one->getId()); $this->assertSame('Another active status', $active_one->getText()); - $list = (new \GaletteObjectsLend\Repository\Status($this->zdb, $this->login))->getActiveStockStatuses(); + $list = (new \GaletteObjectsLend\Repository\Status($this->zdb, $this->preferences, $this->login))->getActiveStockStatuses(); $this->assertCount(1, $list); $active_one = $list[0]; diff --git a/tests/GaletteObjectsLend/Repository/tests/units/Categories.php b/tests/GaletteObjectsLend/Repository/tests/units/Categories.php index 61b4ef3a..9bec8257 100644 --- a/tests/GaletteObjectsLend/Repository/tests/units/Categories.php +++ b/tests/GaletteObjectsLend/Repository/tests/units/Categories.php @@ -39,7 +39,7 @@ public function tearDown(): void */ public function testGetList(): void { - $categories = new \GaletteObjectsLend\Repository\Categories($this->zdb, $this->login); + $categories = new \GaletteObjectsLend\Repository\Categories($this->zdb, $this->preferences, $this->login); $rs_list = $categories->getList(); $this->assertInstanceOf(\Laminas\Db\ResultSet\ResultSet::class, $rs_list); @@ -60,6 +60,7 @@ public function testGetList(): void $category->setName('Another category'); $category->setActive(true); $this->assertTrue($category->store()); + $cat_two_id = $category->getId(); $category = new \GaletteObjectsLend\Entity\LendCategory($this->zdb); $category->setName('Yet another category'); @@ -67,7 +68,7 @@ public function testGetList(): void $this->assertTrue($category->store()); $filters = new \GaletteObjectsLend\Filters\CategoriesList(); - $categories = new \GaletteObjectsLend\Repository\Categories($this->zdb, $this->login, $filters); + $categories = new \GaletteObjectsLend\Repository\Categories($this->zdb, $this->preferences, $this->login, $filters); $this->assertCount(3, $categories->getCategoriesList(true)); $this->assertSame(3, $categories->getCount()); @@ -111,5 +112,15 @@ public function testGetList(): void $filters->reinit(); $this->assertCount(3, $categories->getCategoriesList(true)); $this->assertSame(3, $categories->getCount()); + + //count takes grouping into account + $object = new \GaletteObjectsLend\Entity\LendObject($this->zdb); + $object->setName('Object in another category'); + $object->setCategoryId($cat_two_id); + $this->assertTrue($object->store()); + + $filters->not_empty = true; + $this->assertCount(2, $categories->getCategoriesList(true)); + $this->assertSame(2, $categories->getCount()); } } diff --git a/tests/GaletteObjectsLend/Repository/tests/units/Objects.php b/tests/GaletteObjectsLend/Repository/tests/units/Objects.php index cf9f426d..888b9f6e 100644 --- a/tests/GaletteObjectsLend/Repository/tests/units/Objects.php +++ b/tests/GaletteObjectsLend/Repository/tests/units/Objects.php @@ -51,7 +51,7 @@ public function tearDown(): void */ public function testGetList(): void { - $objects = new \GaletteObjectsLend\Repository\Objects($this->zdb, $this->lend_prefs); + $objects = new \GaletteObjectsLend\Repository\Objects($this->zdb, $this->preferences, $this->login, $this->lend_prefs); $rs_list = $objects->getList(); $this->assertInstanceOf(\Laminas\Db\ResultSet\ResultSet::class, $rs_list); @@ -108,7 +108,7 @@ public function testGetList(): void $missing_id = $object->getId() + 1; $filters = new \GaletteObjectsLend\Filters\ObjectsList(); - $objects = new \GaletteObjectsLend\Repository\Objects($this->zdb, $this->lend_prefs, $filters); + $objects = new \GaletteObjectsLend\Repository\Objects($this->zdb, $this->preferences, $this->login, $this->lend_prefs, $filters); $this->assertCount(4, $objects->getObjectsList(true)); $this->assertSame(4, $objects->getCount()); diff --git a/tests/GaletteObjectsLend/Repository/tests/units/Status.php b/tests/GaletteObjectsLend/Repository/tests/units/Status.php index a62b45f6..7f8aeaf3 100644 --- a/tests/GaletteObjectsLend/Repository/tests/units/Status.php +++ b/tests/GaletteObjectsLend/Repository/tests/units/Status.php @@ -48,7 +48,7 @@ public function tearDown(): void */ public function testGetList(): void { - $status = new \GaletteObjectsLend\Repository\Status($this->zdb, $this->login); + $status = new \GaletteObjectsLend\Repository\Status($this->zdb, $this->preferences, $this->login); $rs_list = $status->getList(); $this->assertInstanceOf(\Laminas\Db\ResultSet\ResultSet::class, $rs_list); @@ -90,7 +90,7 @@ public function testGetList(): void $this->assertTrue($status->store()); $filters = new \GaletteObjectsLend\Filters\StatusList(); - $status = new \GaletteObjectsLend\Repository\Status($this->zdb, $this->login, $filters); + $status = new \GaletteObjectsLend\Repository\Status($this->zdb, $this->preferences, $this->login, $filters); $this->assertCount(5, $status->getStatusList(true)); $this->assertSame(5, $status->getCount()); diff --git a/tests/GaletteObjectsLend/tests/units/LendService.php b/tests/GaletteObjectsLend/tests/units/LendService.php index 8241011b..0291f9a6 100644 --- a/tests/GaletteObjectsLend/tests/units/LendService.php +++ b/tests/GaletteObjectsLend/tests/units/LendService.php @@ -109,7 +109,7 @@ public function tearDown(): void */ private function getService(): \GaletteObjectsLend\LendService { - return new \GaletteObjectsLend\LendService($this->zdb, $this->login, new Preferences($this->zdb)); + return new \GaletteObjectsLend\LendService($this->zdb, $this->preferences, $this->login, new Preferences($this->zdb)); } /** From c30713a5ea794395437ea6d7bf454515d5d6b150 Mon Sep 17 00:00:00 2001 From: Johan Cwiklinski Date: Wed, 23 Sep 2026 10:45:18 +0200 Subject: [PATCH 2/6] Share search and activity filters between lists --- .../Filters/CategoriesList.php | 97 ++-------- .../Filters/ListFilters.php | 163 ++++++++++++++++ .../Filters/ObjectsList.php | 183 +++++------------- lib/GaletteObjectsLend/Filters/StatusList.php | 115 ++--------- phpstan-baseline.neon | 6 - .../Filters/tests/units/CategoriesList.php | 3 +- .../Filters/tests/units/ObjectsList.php | 2 +- .../Filters/tests/units/StatusList.php | 4 +- 8 files changed, 255 insertions(+), 318 deletions(-) create mode 100644 lib/GaletteObjectsLend/Filters/ListFilters.php diff --git a/lib/GaletteObjectsLend/Filters/CategoriesList.php b/lib/GaletteObjectsLend/Filters/CategoriesList.php index 1cbe9d59..68fd044c 100644 --- a/lib/GaletteObjectsLend/Filters/CategoriesList.php +++ b/lib/GaletteObjectsLend/Filters/CategoriesList.php @@ -10,7 +10,6 @@ namespace GaletteObjectsLend\Filters; -use Analog\Analog; use Galette\Core\Pagination; use GaletteObjectsLend\Repository\Categories; @@ -24,23 +23,12 @@ * @property ?bool $not_empty * @property ?ObjectsList $objects_filters */ - class CategoriesList extends Pagination { - //filters - private ?string $filter_str; - private ?int $active_filter; - private ?bool $not_empty; - private ?ObjectsList $objects_filters; - + use ListFilters; - /** @var array */ - protected array $categorylist_fields = [ - 'filter_str', - 'active_filter', - 'not_empty', - 'objects_filters' - ]; + private ?bool $not_empty = null; + private ?ObjectsList $objects_filters = null; /** * Default constructor @@ -64,87 +52,34 @@ protected function getDefaultOrder(): int|string public function reinit(): void { parent::reinit(); - $this->filter_str = null; - $this->active_filter = null; + $this->reinitListFilters(); $this->not_empty = null; $this->objects_filters = null; } /** - * Global getter method + * Filtering properties of the class, besides filter_str and active_filter * - * @param string $name name of the property we want to retrieve - * - * @return mixed the called property + * @return array */ - public function __get(string $name): mixed + protected function getOwnFilters(): array { - if (in_array($name, $this->pagination_fields)) { - return parent::__get($name); - } else { - if (in_array($name, $this->categorylist_fields)) { - return $this->$name; - } - } - - throw new \RuntimeException( - sprintf( - 'Unable to get property "%s::%s"!', - __CLASS__, - $name - ) - ); + return ['not_empty', 'objects_filters']; } /** - * Global setter method + * Set a filtering property of the class * - * @param string $name name of the property we want to assign a value to - * @param mixed $value a relevant value for the property + * @param string $name Property name + * @param mixed $value Value */ - public function __set(string $name, mixed $value): void + protected function setOwnFilter(string $name, mixed $value): bool { - - if (in_array($name, $this->pagination_fields)) { - parent::__set($name, $value); - } else { - Analog::log( - '[CategoriesList] Setting property `' . $name . '`', - Analog::DEBUG - ); - - switch ($name) { - case 'filter_str': - case 'not_empty': - $this->$name = $value; - break; - case 'active_filter': - switch ($value) { - case Categories::ALL_CATEGORIES: - case Categories::ACTIVE_CATEGORIES: - case Categories::INACTIVE_CATEGORIES: - $this->active_filter = (int)$value; - break; - default: - Analog::log( - '[CategoriesList] Value for active filter should be either ' - . Categories::ALL_CATEGORIES . ', ' . Categories::ACTIVE_CATEGORIES . ' or ' - . Categories::INACTIVE_CATEGORIES . ' (' . $value . ' given)', - Analog::WARNING - ); - break; - } - break; - default: - throw new \RuntimeException( - sprintf( - 'Unable to set property "%s::%s"!', - __CLASS__, - $name - ) - ); - } + if ($name !== 'not_empty') { + return false; } + $this->not_empty = $value === null ? null : (bool)$value; + return true; } /** diff --git a/lib/GaletteObjectsLend/Filters/ListFilters.php b/lib/GaletteObjectsLend/Filters/ListFilters.php new file mode 100644 index 00000000..e56de44d --- /dev/null +++ b/lib/GaletteObjectsLend/Filters/ListFilters.php @@ -0,0 +1,163 @@ + + */ +trait ListFilters +{ + private ?string $filter_str = null; + private ?int $active_filter = null; + + /** + * Filtering properties of the class, besides filter_str and active_filter + * + * @return array + */ + abstract protected function getOwnFilters(): array; + + /** + * Set a filtering property of the class + * + * @param string $name Property name + * @param mixed $value Value + * + * @return bool false if property is unknown + */ + abstract protected function setOwnFilter(string $name, mixed $value): bool; + + /** + * Reinit shared filters + */ + protected function reinitListFilters(): void + { + $this->filter_str = null; + $this->active_filter = null; + } + + /** + * Global isset method + * + * @param string $name Property name + */ + public function __isset(string $name): bool + { + return in_array($name, $this->pagination_fields) || in_array($name, $this->getFilterNames()); + } + + /** + * Global getter method + * + * @param string $name name of the property we want to retrieve + * + * @return mixed the called property + */ + public function __get(string $name): mixed + { + if (in_array($name, $this->pagination_fields)) { + return parent::__get($name); + } + if (in_array($name, $this->getFilterNames())) { + return $this->$name; + } + + throw new \RuntimeException( + sprintf( + 'Unable to get property "%s::%s"!', + static::class, + $name + ) + ); + } + + /** + * Global setter method + * + * @param string $name name of the property we want to assign a value to + * @param mixed $value a relevant value for the property + */ + public function __set(string $name, mixed $value): void + { + if (in_array($name, $this->pagination_fields)) { + parent::__set($name, $value); + return; + } + + switch ($name) { + case 'filter_str': + $this->filter_str = $value === null ? null : (string)$value; + break; + case 'active_filter': + $this->active_filter = $this->toChoice( + $name, + $value, + [AbstractRepository::ALL, AbstractRepository::ACTIVE, AbstractRepository::INACTIVE] + ) ?? $this->active_filter; + break; + default: + if (!$this->setOwnFilter($name, $value)) { + throw new \RuntimeException( + sprintf( + 'Unable to set property "%s::%s"!', + static::class, + $name + ) + ); + } + } + } + + /** + * Check a value is one of the allowed choices + * + * @param string $name Property name + * @param mixed $value Value + * @param array $choices Allowed values + * + * @return ?int Value, null if it is not allowed + */ + protected function toChoice(string $name, mixed $value, array $choices): ?int + { + if (is_numeric($value) && in_array((int)$value, $choices, true)) { + return (int)$value; + } + + Analog::log( + sprintf( + '[%1$s] Value for %2$s should be one of %3$s (%4$s given)', + static::class, + $name, + implode(', ', $choices), + var_export($value, true) + ), + Analog::WARNING + ); + return null; + } + + /** + * Names of all filtering properties + * + * @return array + */ + private function getFilterNames(): array + { + return array_merge(['filter_str', 'active_filter'], $this->getOwnFilters()); + } +} diff --git a/lib/GaletteObjectsLend/Filters/ObjectsList.php b/lib/GaletteObjectsLend/Filters/ObjectsList.php index c244e4ff..bbc0967f 100644 --- a/lib/GaletteObjectsLend/Filters/ObjectsList.php +++ b/lib/GaletteObjectsLend/Filters/ObjectsList.php @@ -21,32 +21,20 @@ * * @author Johan Cwiklinski * - * @property ?string $filter_str - * @property ?int $category_filter - * @property ?int $active_filter - * @property ?int $field_filter - * @property array $selected + * @property ?string $filter_str + * @property ?int $category_filter + * @property ?int $active_filter + * @property ?int $field_filter + * @property array $selected */ - class ObjectsList extends Pagination { - //filters - private ?string $filter_str; - private ?int $category_filter; - private ?int $active_filter; - private ?int $field_filter; - /** @var array */ - private array $selected; - + use ListFilters; - /** @var array */ - protected array $objectslist_fields = [ - 'filter_str', - 'category_filter', - 'active_filter', - 'field_filter', - 'selected' - ]; + private ?int $category_filter = null; + private ?int $field_filter = null; + /** @var array */ + private array $selected = []; /** * Default constructor @@ -70,9 +58,8 @@ protected function getDefaultOrder(): int|string public function reinit(): void { parent::reinit(); - $this->filter_str = null; + $this->reinitListFilters(); $this->category_filter = null; - $this->active_filter = null; $this->field_filter = null; $this->selected = []; } @@ -113,128 +100,64 @@ public function highlight(string $html, string $field): string } /** - * Default isset + * Filtering properties of the class, besides filter_str and active_filter * - * @param string $name Property name + * @return array */ - public function __isset(string $name): bool + protected function getOwnFilters(): array { - if (in_array($name, $this->objectslist_fields)) { - return true; - } - - return parent::__isset($name); + return ['category_filter', 'field_filter', 'selected']; } /** - * Global getter method - * - * @param string $name name of the property we want to retrieve + * Set a filtering property of the class * - * @return mixed the called property + * @param string $name Property name + * @param mixed $value Value */ - public function __get(string $name): mixed + protected function setOwnFilter(string $name, mixed $value): bool { - if (in_array($name, $this->pagination_fields)) { - return parent::__get($name); - } else { - if (in_array($name, $this->objectslist_fields)) { - return $this->$name; - } + switch ($name) { + case 'selected': + if (is_array($value)) { + $this->selected = array_map('intval', $value); + } elseif ($value !== null) { + $this->warnType($name, 'an array', $value); + } + return true; + case 'category_filter': + case 'field_filter': + if (is_numeric($value)) { + $this->$name = (int)$value; + } elseif ($value === null) { + $this->$name = null; + } else { + $this->warnType($name, 'an integer', $value); + } + return true; } - - throw new \RuntimeException( - sprintf( - 'Unable to get property "%s::%s"!', - __CLASS__, - $name - ) - ); + return false; } /** - * Global setter method + * Log a value of wrong type * - * @param string $name name of the property we want to assign a value to - * @param mixed $value a relevant value for the property + * @param string $name Property name + * @param string $expected Expected type + * @param mixed $value Value */ - public function __set(string $name, mixed $value): void + private function warnType(string $name, string $expected, mixed $value): void { - - if (in_array($name, $this->pagination_fields)) { - parent::__set($name, $value); - } else { - Analog::log( - '[ObjectsList] Setting property `' . $name . '`', - Analog::DEBUG - ); - - switch ($name) { - case 'selected': - if (is_array($value)) { - $this->$name = $value; - } elseif ($value !== null) { - Analog::log( - '[ObjectsList] Value for property `' . $name - . '` should be an array (' . gettype($value) . ' given)', - Analog::WARNING - ); - } - break; - case 'filter_str': - $this->$name = $value; - break; - case 'category_filter': - if (is_numeric($value)) { - $this->$name = (int)$value; - } elseif ($value !== null) { - Analog::log( - '[ObjectsList] Value for property `' . $name - . '` should be an integer (' . gettype($value) . ' given)', - Analog::WARNING - ); - } else { - $this->$name = null; - } - break; - case 'active_filter': - switch ($value) { - case Objects::ALL_OBJECTS: - case Objects::ACTIVE_OBJECTS: - case Objects::INACTIVE_OBJECTS: - $this->active_filter = (int)$value; - break; - default: - Analog::log( - '[ObjectsList] Value for active filter should be either ' - . Objects::ACTIVE_OBJECTS . ', ' . Objects::ACTIVE_OBJECTS . ' or ' - . Objects::INACTIVE_OBJECTS . ' (' . $value . ' given)', - Analog::WARNING - ); - break; - } - break; - case 'field_filter': - if (is_numeric($value)) { - $this->$name = (int)$value; - } elseif ($value !== null) { - Analog::log( - '[ObjectsList] Value for property `' . $name - . '` should be an integer (' . gettype($value) . ' given)', - Analog::WARNING - ); - } - break; - default: - throw new \RuntimeException( - sprintf( - 'Unable to set property "%s::%s"!', - __CLASS__, - $name - ) - ); - } - } + Analog::log( + sprintf( + '[%1$s] Value for %2$s should be %3$s (%4$s given)', + static::class, + $name, + $expected, + gettype($value) + ), + Analog::WARNING + ); } /** diff --git a/lib/GaletteObjectsLend/Filters/StatusList.php b/lib/GaletteObjectsLend/Filters/StatusList.php index a60b31f0..64ceaf34 100644 --- a/lib/GaletteObjectsLend/Filters/StatusList.php +++ b/lib/GaletteObjectsLend/Filters/StatusList.php @@ -10,7 +10,6 @@ namespace GaletteObjectsLend\Filters; -use Analog\Analog; use Galette\Core\Pagination; use GaletteObjectsLend\Repository\Status; @@ -23,21 +22,11 @@ * @property ?int $active_filter * @property ?int $stock_filter */ - class StatusList extends Pagination { - //filters - private ?string $filter_str; - private ?int $active_filter; - private ?int $stock_filter; - + use ListFilters; - /** @var array */ - protected array $statuslist_fields = [ - 'filter_str', - 'active_filter', - 'stock_filter' - ]; + private ?int $stock_filter = null; /** * Returns the field we want to default set order to @@ -53,102 +42,36 @@ protected function getDefaultOrder(): int|string public function reinit(): void { parent::reinit(); - $this->filter_str = null; - $this->active_filter = null; + $this->reinitListFilters(); $this->stock_filter = null; } /** - * Global getter method + * Filtering properties of the class, besides filter_str and active_filter * - * @param string $name name of the property we want to retrieve - * - * @return mixed the called property + * @return array */ - public function __get(string $name): mixed + protected function getOwnFilters(): array { - if (in_array($name, $this->pagination_fields)) { - return parent::__get($name); - } else { - if (in_array($name, $this->statuslist_fields)) { - return $this->$name; - } - } - - throw new \RuntimeException( - sprintf( - 'Unable to get property "%s::%s"!', - __CLASS__, - $name - ) - ); + return ['stock_filter']; } /** - * Global setter method + * Set a filtering property of the class * - * @param string $name name of the property we want to assign a value to - * @param mixed $value a relevant value for the property + * @param string $name Property name + * @param mixed $value Value */ - public function __set(string $name, mixed $value): void + protected function setOwnFilter(string $name, mixed $value): bool { - - if (in_array($name, $this->pagination_fields)) { - parent::__set($name, $value); - } else { - Analog::log( - '[StatusList] Setting property `' . $name . '`', - Analog::DEBUG - ); - - switch ($name) { - case 'filter_str': - $this->$name = $value; - break; - case 'active_filter': - switch ($value) { - case Status::ALL: - case Status::ACTIVE: - case Status::INACTIVE: - $this->active_filter = (int)$value; - break; - default: - Analog::log( - '[StatusList] Value for active filter should be either ' - . Status::ACTIVE . ' or ' - . Status::INACTIVE . ' (' . $value . ' given)', - Analog::WARNING - ); - break; - } - break; - case 'stock_filter': - switch ($value) { - case Status::DC_STOCK: - case Status::IN_STOCK: - case Status::OUT_STOCK: - $this->stock_filter = (int)$value; - break; - default: - Analog::log( - '[StatusList] Value for stock filter should be either ' - . Status::IN_STOCK . ', ' . Status::OUT_STOCK . ' or ' - . Status::DC_STOCK . ' (' . $value . ' given)', - Analog::WARNING - ); - break; - } - - break; - default: - throw new \RuntimeException( - sprintf( - 'Unable to set property "%s::%s"!', - __CLASS__, - $name - ) - ); - } + if ($name !== 'stock_filter') { + return false; } + $this->stock_filter = $this->toChoice( + $name, + $value, + [Status::DC_STOCK, Status::IN_STOCK, Status::OUT_STOCK] + ) ?? $this->stock_filter; + return true; } } diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 823140cb..10f2856b 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -42,12 +42,6 @@ parameters: count: 1 path: lib/GaletteObjectsLend/Entity/Preferences.php - - - message: '#^Class GaletteObjectsLend\\Filters\\ObjectsList has PHPDoc tag @property for property \$selected with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: lib/GaletteObjectsLend/Filters/ObjectsList.php - - message: '#^Parameter \#5 \$ln of method TCPDF\:\:Cell\(\) expects int, false given\.$#' identifier: argument.type diff --git a/tests/GaletteObjectsLend/Filters/tests/units/CategoriesList.php b/tests/GaletteObjectsLend/Filters/tests/units/CategoriesList.php index 1c84c389..74376cab 100644 --- a/tests/GaletteObjectsLend/Filters/tests/units/CategoriesList.php +++ b/tests/GaletteObjectsLend/Filters/tests/units/CategoriesList.php @@ -84,8 +84,7 @@ public function testCreate(): void $filters->active_filter = 42; $this->expectLogEntry( Analog::WARNING, - '[CategoriesList] Value for active filter should be either 0, 1 or 2 (42 given) -' + '[GaletteObjectsLend\Filters\CategoriesList] Value for active_filter should be one of 0, 1, 2 (42 given)' ); $this->assertSame(\GaletteObjectsLend\Repository\Categories::INACTIVE_CATEGORIES, $filters->active_filter); diff --git a/tests/GaletteObjectsLend/Filters/tests/units/ObjectsList.php b/tests/GaletteObjectsLend/Filters/tests/units/ObjectsList.php index d8cdc5fb..5c1fd8e3 100644 --- a/tests/GaletteObjectsLend/Filters/tests/units/ObjectsList.php +++ b/tests/GaletteObjectsLend/Filters/tests/units/ObjectsList.php @@ -81,7 +81,7 @@ public function testCreate(): void $filters->active_filter = 42; $this->expectLogEntry( Analog::WARNING, - '[ObjectsList] Value for active filter should be either 1, 1 or 2 (42 given)' + '[GaletteObjectsLend\Filters\ObjectsList] Value for active_filter should be one of 0, 1, 2 (42 given)' ); $this->assertSame(\GaletteObjectsLend\Repository\Objects::INACTIVE_OBJECTS, $filters->active_filter); diff --git a/tests/GaletteObjectsLend/Filters/tests/units/StatusList.php b/tests/GaletteObjectsLend/Filters/tests/units/StatusList.php index 408b7a4a..cbcdc8b2 100644 --- a/tests/GaletteObjectsLend/Filters/tests/units/StatusList.php +++ b/tests/GaletteObjectsLend/Filters/tests/units/StatusList.php @@ -79,7 +79,7 @@ public function testCreate(): void $filters->active_filter = 42; $this->expectLogEntry( Analog::WARNING, - '[StatusList] Value for active filter should be either 1 or 2 (42 given)' + '[GaletteObjectsLend\Filters\StatusList] Value for active_filter should be one of 0, 1, 2 (42 given)' ); $this->assertSame(\GaletteObjectsLend\Repository\Status::INACTIVE, $filters->active_filter); @@ -94,7 +94,7 @@ public function testCreate(): void $filters->stock_filter = 42; $this->expectLogEntry( Analog::WARNING, - '[StatusList] Value for stock filter should be either 1, 2 or 0 (42 given)' + '[GaletteObjectsLend\Filters\StatusList] Value for stock_filter should be one of 0, 1, 2 (42 given)' ); $this->assertSame(\GaletteObjectsLend\Repository\Status::OUT_STOCK, $filters->stock_filter); From 81e6d1561b08c7cd0a5cf7e93d8b3050b4c6d1a8 Mon Sep 17 00:00:00 2001 From: Johan Cwiklinski Date: Wed, 23 Sep 2026 10:51:55 +0200 Subject: [PATCH 3/6] Share list, filter, edit and delete actions between categories and statuses --- .../Crud/AbstractListController.php | 370 ++++++++++++++++++ .../Controllers/Crud/CategoriesController.php | 348 +++++----------- .../Controllers/Crud/ObjectsController.php | 60 +-- .../Controllers/Crud/StatusController.php | 331 ++++------------ .../Controllers/PdfController.php | 8 +- .../tests/units/CategoriesController.php | 148 +++++++ .../tests/units/ObjectsController.php | 2 +- .../tests/units/StatusController.php | 173 ++++++++ 8 files changed, 910 insertions(+), 530 deletions(-) create mode 100644 lib/GaletteObjectsLend/Controllers/Crud/AbstractListController.php create mode 100644 tests/GaletteObjectsLend/Controllers/tests/units/CategoriesController.php create mode 100644 tests/GaletteObjectsLend/Controllers/tests/units/StatusController.php diff --git a/lib/GaletteObjectsLend/Controllers/Crud/AbstractListController.php b/lib/GaletteObjectsLend/Controllers/Crud/AbstractListController.php new file mode 100644 index 00000000..6a4bd1dd --- /dev/null +++ b/lib/GaletteObjectsLend/Controllers/Crud/AbstractListController.php @@ -0,0 +1,370 @@ + + * + * @template TEntity of LendCategory|LendStatus + * @template TFilters of CategoriesList|StatusList + */ +abstract class AbstractListController extends AbstractPluginController +{ + /** + * @var array + */ + #[Inject("Plugin Galette Objects Lend")] + protected array $module_info; + + /** + * Routes name part: objectslend_{route}_edit, objectslend_doremove_{route}... + */ + abstract protected function getEntityRouteName(): string; + + /** + * Name of the list route + */ + abstract protected function getListRouteName(): string; + + /** + * Create empty filters + * + * @return TFilters + */ + abstract protected function createFilters(): Pagination; + + /** + * Apply posted filters specific to the list + * + * @param TFilters $filters Filters + * @param array $post Posted values + */ + protected function applyOwnFilters(Pagination $filters, array $post): void + { + } + + /** + * Get list template name and parameters + * + * @param TFilters $filters Filters + * + * @return array{0: string, 1: array} + */ + abstract protected function getListView(Pagination $filters): array; + + /** + * Load an entity + * + * @param ?int $id Entity ID, null for a new one + * + * @return TEntity + */ + abstract protected function loadEntity(?int $id): LendCategory|LendStatus; + + /** + * Fill entity from posted values + * + * @param TEntity $entity Entity + * @param array $post Posted values + */ + abstract protected function fillEntity(LendCategory|LendStatus $entity, array $post): void; + + /** + * Get edit template name and parameters + * + * @param TEntity $entity Entity + * @param string $action Either add or edit + * + * @return array{0: string, 1: array} + */ + abstract protected function getEditView(LendCategory|LendStatus $entity, string $action): array; + + /** + * Additional work once the entity has been stored + * + * @param TEntity $entity Entity + * @param Request $request PSR Request + * @param array $post Posted values + * + * @return array Errors + */ + protected function afterStore(LendCategory|LendStatus $entity, Request $request, array $post): array + { + return []; + } + + /** + * Message displayed once the entity has been stored + */ + abstract protected function getStoredMessage(): string; + + /** + * Message displayed when the entity cannot be stored + */ + abstract protected function getStoreErrorMessage(): string; + + /** + * Get filters from session + * + * @return TFilters + */ + protected function getFilters(): Pagination + { + return $this->session->{$this->getFilterName(static::getDefaultFilterName())} + ?? $this->createFilters(); + } + + /** + * Store filters in session + * + * @param TFilters $filters Filters + */ + protected function storeFilters(Pagination $filters): void + { + $this->session->{$this->getFilterName(static::getDefaultFilterName())} = $filters; + } + + /** + * Default filter name, used to store filters in session + */ + abstract public static function getDefaultFilterName(): string; + + // CRUD - Create + + /** + * Add page + * + * @param Request $request PSR Request + * @param Response $response PSR Response + */ + public function add(Request $request, Response $response): Response + { + return $this->edit($request, $response, null, 'add'); + } + + /** + * Add action + * + * @param Request $request PSR Request + * @param Response $response PSR Response + */ + public function doAdd(Request $request, Response $response): Response + { + return $this->doEdit($request, $response, null, 'add'); + } + + // /CRUD - Create + // CRUD - Read + + /** + * List page + * + * @param Request $request PSR Request + * @param Response $response PSR Response + * @param string|null $option One of 'page' or 'order' + * @param int|string|null $value Value of the option + */ + public function list(Request $request, Response $response, ?string $option = null, int|string|null $value = null): Response + { + $filters = $this->getFilters(); + + if ($option === 'page') { + $filters->current_page = (int)$value; + } elseif ($option === 'order') { + $filters->orderby = $value; + } + + [$template, $params] = $this->getListView($filters); + + $this->storeFilters($filters); + + //assign pagination variables to the template and add pagination links + $filters->setViewPagination($this->routeparser, $this->view, false); + + $this->view->render( + $response, + $this->getTemplate($template), + $params + [ + 'require_dialog' => true, + 'filters' => $filters + ] + ); + return $response; + } + + /** + * Filtering + * + * @param Request $request PSR Request + * @param Response $response PSR Response + */ + public function filter(Request $request, Response $response): Response + { + $post = $request->getParsedBody(); + $filters = $this->getFilters(); + + if (isset($post['clear_filter'])) { + $filters->reinit(); + } else { + if (isset($post['filter_str'])) { + $filters->filter_str = $post['filter_str']; + } + if (isset($post['active_filter']) && is_numeric($post['active_filter'])) { + $filters->active_filter = $post['active_filter']; + } + $this->applyOwnFilters($filters, $post); + if (isset($post['nbshow'])) { + $filters->show = $post['nbshow']; + } + } + + $this->storeFilters($filters); + + return $response + ->withStatus(301) + ->withHeader('Location', $this->routeparser->urlFor($this->getListRouteName())); + } + + // /CRUD - Read + // CRUD - Update + + /** + * Edit page + * + * @param Request $request PSR Request + * @param Response $response PSR Response + * @param int|null $id Model id + * @param string $action Action + */ + public function edit(Request $request, Response $response, ?int $id = null, string $action = 'edit'): Response + { + $entity = $this->loadEntity($id); + //values posted before an error + $data_key = $this->getDataSessionKey(); + $data = $this->session->$data_key ?? null; + if (is_array($data)) { + $this->fillEntity($entity, $data); + } + unset($this->session->$data_key); + + [$template, $params] = $this->getEditView($entity, $action); + $this->view->render( + $response, + $this->getTemplate($template), + $params + ['action' => $action] + ); + return $response; + } + + /** + * Edit action + * + * @param Request $request PSR Request + * @param Response $response PSR Response + * @param null|int $id Model id for edit + * @param string $action Either add or edit + */ + public function doEdit(Request $request, Response $response, ?int $id = null, string $action = 'edit'): Response + { + $post = $request->getParsedBody(); + $entity = $this->loadEntity($id); + $this->fillEntity($entity, $post); + + if ($entity->store()) { + $errors = $this->afterStore($entity, $request, $post); + } else { + $errors = [$this->getStoreErrorMessage()]; + } + + if (count($errors) === 0) { + $this->flash->addMessage('success_detected', $this->getStoredMessage()); + return $response + ->withStatus(301) + ->withHeader('Location', $this->routeparser->urlFor($this->getListRouteName())); + } + + $data_key = $this->getDataSessionKey(); + $this->session->$data_key = $post; + foreach ($errors as $error) { + $this->flash->addMessage('error_detected', $error); + } + + //entity may have been stored before the error + $route = 'objectslend_' . $this->getEntityRouteName(); + return $response + ->withStatus(301) + ->withHeader( + 'Location', + $entity->getId() === null + ? $this->routeparser->urlFor($route . '_add') + : $this->routeparser->urlFor($route . '_edit', ['id' => (string)$entity->getId()]) + ); + } + + /** + * Session key for values posted before an error + */ + private function getDataSessionKey(): string + { + return 'objectslend_' . $this->getEntityRouteName() . '_data'; + } + + // /CRUD - Update + // CRUD - Delete + + /** + * Get redirection URI + * + * @param array $args Route arguments + */ + public function redirectUri(array $args): string + { + return $this->routeparser->urlFor($this->getListRouteName()); + } + + /** + * Get form URI + * + * @param array $args Route arguments + */ + public function formUri(array $args): string + { + return $this->routeparser->urlFor( + 'objectslend_doremove_' . $this->getEntityRouteName(), + $args + ); + } + + /** + * Remove entity + * + * @param array $args Route arguments + * @param array $post POST values + */ + protected function doDelete(array $args, array $post): bool + { + return $this->loadEntity((int)$args['id'])->delete(); + } + + // /CRUD - Delete +} diff --git a/lib/GaletteObjectsLend/Controllers/Crud/CategoriesController.php b/lib/GaletteObjectsLend/Controllers/Crud/CategoriesController.php index 88670f7f..fdb274d5 100644 --- a/lib/GaletteObjectsLend/Controllers/Crud/CategoriesController.php +++ b/lib/GaletteObjectsLend/Controllers/Crud/CategoriesController.php @@ -11,310 +11,168 @@ namespace GaletteObjectsLend\Controllers\Crud; use Analog\Analog; -use DI\Attribute\Inject; +use Galette\Core\Pagination; use GaletteObjectsLend\Entity\CategoryPicture; -use GaletteObjectsLend\Filters\CategoriesList; -use GaletteObjectsLend\Repository\Categories; use GaletteObjectsLend\Entity\LendCategory; +use GaletteObjectsLend\Entity\LendStatus; use GaletteObjectsLend\Entity\Preferences; -use Galette\Controllers\Crud\AbstractPluginController; +use GaletteObjectsLend\Filters\CategoriesList; +use GaletteObjectsLend\Repository\Categories; use Slim\Psr7\Request; -use Slim\Psr7\Response; /** * Categories controller * * @author Johan Cwiklinski + * + * @extends AbstractListController */ - -class CategoriesController extends AbstractPluginController +class CategoriesController extends AbstractListController { /** - * @var array + * Default filter name, used to store filters in session */ - #[Inject("Plugin Galette Objects Lend")] - protected array $module_info; - - // CRUD - Create + public static function getDefaultFilterName(): string + { + return 'categories'; + } /** - * Add page - * - * @param Request $request PSR Request - * @param Response $response PSR Response + * Routes name part */ - public function add(Request $request, Response $response): Response + protected function getEntityRouteName(): string { - return $this->edit($request, $response, null, 'add'); + return 'category'; } /** - * Add action - * - * @param Request $request PSR Request - * @param Response $response PSR Response + * Name of the list route */ - public function doAdd(Request $request, Response $response): Response + protected function getListRouteName(): string { - return $this->doEdit($request, $response, null, 'add'); + return 'objectslend_categories'; } - // /CRUD - Create - // CRUD - Read + /** + * Create empty filters + */ + protected function createFilters(): CategoriesList + { + return new CategoriesList(); + } /** - * List page + * Get list template name and parameters * - * @param Request $request PSR Request - * @param Response $response PSR Response - * @param string|null $option One of 'page' or 'order' - * @param int|string|null $value Value of the option + * @param CategoriesList $filters Filters + * + * @return array{0: string, 1: array} */ - public function list(Request $request, Response $response, ?string $option = null, int|string|null $value = null): Response + protected function getListView(Pagination $filters): array { - if (isset($this->session->objectslend_filter_categories)) { - $filters = $this->session->objectslend_filter_categories; - } else { - $filters = new CategoriesList(); - } - - if ($option !== null) { - switch ($option) { - case 'page': - $filters->current_page = (int)$value; - break; - case 'order': - $filters->orderby = $value; - break; - } - } - $categories = new Categories($this->zdb, $this->preferences, $this->login, $filters); $list = $categories->getCategoriesList(true); - $this->session->objectslend_filter_categories = $filters; - - //assign pagination variables to the template and add pagination links - $filters->setViewPagination($this->routeparser, $this->view, false); - - $lendsprefs = new Preferences($this->zdb); - // display page - $this->view->render( - $response, - $this->getTemplate('categories_list'), + return [ + 'categories_list', [ - 'page_title' => _T("Categories list", "objectslend"), - 'require_dialog' => true, - 'categories' => $list, - 'nb_categories' => count($list), - 'filters' => $filters, - 'olendsprefs' => $lendsprefs, - 'time' => time() + 'page_title' => _T("Categories list", "objectslend"), + 'categories' => $list, + 'nb_categories' => count($list), + 'olendsprefs' => new Preferences($this->zdb), + 'time' => time() ] - ); - return $response; + ]; } /** - * Filtering + * Load an entity * - * @param Request $request PSR Request - * @param Response $response PSR Response + * @param ?int $id Entity ID, null for a new one */ - public function filter(Request $request, Response $response): Response + protected function loadEntity(?int $id): LendCategory { - $post = $request->getParsedBody(); - if (isset($this->session->objectslend_filter_categories)) { - $filters = $this->session->objectslend_filter_categories; - } else { - $filters = new CategoriesList(); - } - - //reintialize filters - if (isset($post['clear_filter'])) { - $filters->reinit(); - } else { - //string to filter - if (isset($post['filter_str'])) { //filter search string - $filters->filter_str = stripslashes( - htmlspecialchars($post['filter_str'], ENT_QUOTES) - ); - } - //activity to filter - if (isset($post['active_filter'])) { - if (is_numeric($post['active_filter'])) { - $filters->active_filter = $post['active_filter']; - } - } - //number of rows to show - if (isset($post['nbshow'])) { - $filters->show = $post['nbshow']; - } - } - - $this->session->objectslend_filter_categories = $filters; - - return $response - ->withStatus(301) - ->withHeader('Location', $this->routeparser->urlFor('objectslend_categories')); + return new LendCategory($this->zdb, $id); } - // /CRUD - Read - // CRUD - Update - /** - * Edit page + * Fill category from posted values * - * @param Request $request PSR Request - * @param Response $response PSR Response - * @param int|null $id Model id - * @param string $action Action + * @param LendCategory $entity Category + * @param array $post Posted values */ - public function edit(Request $request, Response $response, ?int $id = null, string $action = 'edit'): Response + protected function fillEntity(LendCategory|LendStatus $entity, array $post): void { - $category = new LendCategory($this->zdb, $id); - //values posted before an error - $data = $this->session->objectslend_category_data ?? null; - if (is_array($data)) { - $this->fillCategory($category, $data); - } - unset($this->session->objectslend_category_data); - - if ($category->getId() !== null) { - $title = _T("Edit category", "objectslend"); - } else { - $title = _T("New category", "objectslend"); - } - - $lendsprefs = new Preferences($this->zdb); - $picture = new CategoryPicture($category->getId()); - $params = [ - 'page_title' => $title, - 'category' => $category, - 'time' => time(), - 'action' => $action, - 'olendsprefs' => $lendsprefs, - 'picture' => $picture - ]; - - // display page - $this->view->render( - $response, - $this->getTemplate('category_edit'), - $params - ); - return $response; + $entity + ->setName($post['name']) + ->setActive(($post['is_active'] ?? false) == true); } /** - * Edit action + * Get edit template name and parameters * - * @param Request $request PSR Request - * @param Response $response PSR Response - * @param null|int $id Model id for edit - * @param string $action Either add or edit + * @param LendCategory $entity Category + * @param string $action Either add or edit + * + * @return array{0: string, 1: array} */ - public function doEdit(Request $request, Response $response, ?int $id = null, string $action = 'edit'): Response + protected function getEditView(LendCategory|LendStatus $entity, string $action): array { - $post = $request->getParsedBody(); - $category = new LendCategory($this->zdb, $id); - $error_detected = []; - - $this->fillCategory($category, $post); - if ($category->store()) { - // picture upload - $picture = new CategoryPicture($category->getId()); - if (!$picture->upload($request->getUploadedFiles(), 'picture')) { - $error_detected = $picture->uploadErrors(); - } - - if (isset($post['del_photo'])) { - if (!$picture->delete()) { - $error_detected[] = _T("Delete failed", "objectslend"); - Analog::log( - 'Unable to delete picture for category ' . $category->getName(false), - Analog::ERROR - ); - } - } - } else { - $error_detected[] = _T("An error occurred while storing the category.", "objectslend"); - } - - $args = ($id === null ? [] : ['id' => $id]); - if (count($error_detected)) { - $this->session->objectslend_category_data = $post; - //category may have been stored before the error - if ($category->getId() !== null) { - $action = 'edit'; - $args = ['id' => (string)$category->getId()]; - } - foreach ($error_detected as $error) { - $this->flash->addMessage( - 'error_detected', - $error - ); - } - - return $response - ->withStatus(301) - ->withHeader( - 'Location', - $this->routeparser->urlFor('objectslend_category_' . $action, $args) - ); - } else { - //redirect to categories list - $this->flash->addMessage( - 'success_detected', - _T("Category has been saved", "objectslend") - ); - - return $response - ->withStatus(301) - ->withHeader( - 'Location', - $this->routeparser->urlFor('objectslend_categories', $args) - ); - } + return [ + 'category_edit', + [ + 'page_title' => $entity->getId() !== null + ? _T("Edit category", "objectslend") + : _T("New category", "objectslend"), + 'category' => $entity, + 'time' => time(), + 'olendsprefs' => new Preferences($this->zdb), + 'picture' => new CategoryPicture($entity->getId()) + ] + ]; } /** - * Fill category from posted values + * Upload or remove picture once the category has been stored + * + * @param LendCategory $entity Category + * @param Request $request PSR Request + * @param array $post Posted values * - * @param LendCategory $category Category - * @param array $post Posted values + * @return array Errors */ - private function fillCategory(LendCategory $category, array $post): void + protected function afterStore(LendCategory|LendStatus $entity, Request $request, array $post): array { - $category - ->setName($post['name']) - ->setActive(($post['is_active'] ?? false) == true); - } + $errors = []; + $picture = new CategoryPicture($entity->getId()); + if (!$picture->upload($request->getUploadedFiles(), 'picture')) { + $errors = $picture->uploadErrors(); + } - // /CRUD - Update - // CRUD - Delete + if (isset($post['del_photo']) && !$picture->delete()) { + $errors[] = _T("Delete failed", "objectslend"); + Analog::log( + 'Unable to delete picture for category #' . $entity->getId(), + Analog::ERROR + ); + } + return $errors; + } /** - * Get redirection URI - * - * @param array $args Route arguments + * Message displayed once the entity has been stored */ - public function redirectUri(array $args): string + protected function getStoredMessage(): string { - return $this->routeparser->urlFor('objectslend_categories'); + return _T("Category has been saved", "objectslend"); } /** - * Get form URI - * - * @param array $args Route arguments + * Message displayed when the entity cannot be stored */ - public function formUri(array $args): string + protected function getStoreErrorMessage(): string { - return $this->routeparser->urlFor( - 'objectslend_doremove_category', - $args - ); + return _T("An error occurred while storing the category.", "objectslend"); } /** @@ -324,25 +182,9 @@ public function formUri(array $args): string */ public function confirmRemoveTitle(array $args): string { - $category = new LendCategory($this->zdb, (int)$args['id']); return sprintf( _T('Remove category %1$s', 'objectslend'), - $category->getName(false) + $this->loadEntity((int)$args['id'])->getName(false) ); } - - /** - * Remove object - * - * @param array $args Route arguments - * @param array $post POST values - */ - protected function doDelete(array $args, array $post): bool - { - $category = new LendCategory($this->zdb, (int)$args['id']); - return $category->delete(); - } - - // /CRUD - Delete - // /CRUD } diff --git a/lib/GaletteObjectsLend/Controllers/Crud/ObjectsController.php b/lib/GaletteObjectsLend/Controllers/Crud/ObjectsController.php index 169530ff..2257d8dd 100644 --- a/lib/GaletteObjectsLend/Controllers/Crud/ObjectsController.php +++ b/lib/GaletteObjectsLend/Controllers/Crud/ObjectsController.php @@ -45,6 +45,30 @@ class ObjectsController extends AbstractPluginController #[Inject("Plugin Galette Objects Lend")] protected array $module_info; + /** + * Default filter name, used to store filters in session + */ + public static function getDefaultFilterName(): string + { + return 'objects'; + } + + /** + * Session key of the filters + */ + private function getFiltersKey(): string + { + return $this->getFilterName(self::getDefaultFilterName()); + } + + /** + * Get filters from session + */ + private function getFilters(): ObjectsList + { + return $this->session->{$this->getFiltersKey()} ?? new ObjectsList(); + } + // CRUD - Create /** @@ -82,11 +106,7 @@ public function doAdd(Request $request, Response $response): Response */ public function list(Request $request, Response $response, ?string $option = null, int|string|null $value = null): Response { - if (isset($this->session->objectslend_filter_objects)) { - $filters = $this->session->objectslend_filter_objects; - } else { - $filters = new ObjectsList(); - } + $filters = $this->getFilters(); if ($option !== null) { switch ($option) { @@ -109,7 +129,7 @@ public function list(Request $request, Response $response, ?string $option = nul $objects = new Objects($this->zdb, $this->preferences, $this->login, $lendsprefs, $filters); $list = $objects->getObjectsList(true); - $this->session->objectslend_filter_objects = $filters; + $this->session->{$this->getFiltersKey()} = $filters; //assign pagination variables to the template and add pagination links $filters->setViewCommonsFilters($lendsprefs, $this->view); @@ -151,11 +171,7 @@ public function list(Request $request, Response $response, ?string $option = nul public function filter(Request $request, Response $response): Response { $post = $request->getParsedBody(); - if (isset($this->session->objectslend_filter_objects)) { - $filters = $this->session->objectslend_filter_objects; - } else { - $filters = new ObjectsList(); - } + $filters = $this->getFilters(); //reintialize filters if (isset($post['clear_filter'])) { @@ -181,7 +197,7 @@ public function filter(Request $request, Response $response): Response } } - $this->session->objectslend_filter_objects = $filters; + $this->session->{$this->getFiltersKey()} = $filters; return $response ->withStatus(301) @@ -225,14 +241,10 @@ public function handleBatch(Request $request, Response $response): Response $post = $request->getParsedBody(); if (isset($post['entries_sel'])) { - if (isset($this->session->objectslend_filter_objects)) { - $filters = $this->session->objectslend_filter_objects; - } else { - $filters = new ObjectsList(); - } + $filters = $this->getFilters(); $filters->selected = $post['entries_sel']; - $this->session->objectslend_filter_objects = $filters; + $this->session->{$this->getFiltersKey()} = $filters; if (isset($post['delete'])) { return $response @@ -834,7 +846,7 @@ protected function getIdsToRemove(array &$args, ?array $post): array|int|null if (isset($args['id'])) { return (int)$args['id']; } else { - $filters = $this->session->objectslend_filter_objects; + $filters = $this->getFilters(); return $filters->selected; } } @@ -855,7 +867,7 @@ public function confirmRemoveTitle(array $args): string ); } else { //batch objects removal - $filters = $this->session->objectslend_filter_objects; + $filters = $this->getFilters(); return str_replace( '%count', count($filters->selected), @@ -872,11 +884,7 @@ public function confirmRemoveTitle(array $args): string */ protected function doDelete(array $args, array $post): bool { - if (isset($this->session->objectslend_filter_objects)) { - $filters = $this->session->objectslend_filter_objects; - } else { - $filters = new ObjectsList(); - } + $filters = $this->getFilters(); $lendsprefs = new Preferences($this->zdb); $objects = new Objects($this->zdb, $this->preferences, $this->login, $lendsprefs, $filters); @@ -888,7 +896,7 @@ protected function doDelete(array $args, array $post): bool $result = $objects->removeObjects($ids); if ($result) { - unset($this->session->objectslend_filter_objects); + unset($this->session->{$this->getFiltersKey()}); } return $result; } diff --git a/lib/GaletteObjectsLend/Controllers/Crud/StatusController.php b/lib/GaletteObjectsLend/Controllers/Crud/StatusController.php index 244b6754..45140372 100644 --- a/lib/GaletteObjectsLend/Controllers/Crud/StatusController.php +++ b/lib/GaletteObjectsLend/Controllers/Crud/StatusController.php @@ -10,83 +10,75 @@ namespace GaletteObjectsLend\Controllers\Crud; -use DI\Attribute\Inject; +use Galette\Core\Pagination; +use GaletteObjectsLend\Entity\LendCategory; +use GaletteObjectsLend\Entity\LendStatus; use GaletteObjectsLend\Filters\StatusList; use GaletteObjectsLend\Repository\Status; -use GaletteObjectsLend\Entity\LendStatus; -use GaletteObjectsLend\Entity\Preferences; -use Galette\Controllers\Crud\AbstractPluginController; -use Slim\Psr7\Request; -use Slim\Psr7\Response; /** * Status controller * * @author Johan Cwiklinski + * + * @extends AbstractListController */ - -class StatusController extends AbstractPluginController +class StatusController extends AbstractListController { /** - * @var array + * Default filter name, used to store filters in session */ - #[Inject("Plugin Galette Objects Lend")] - protected array $module_info; - - // CRUD - Create + public static function getDefaultFilterName(): string + { + return 'statuses'; + } /** - * Add page - * - * @param Request $request PSR Request - * @param Response $response PSR Response + * Routes name part */ - public function add(Request $request, Response $response): Response + protected function getEntityRouteName(): string { - return $this->edit($request, $response, null, 'add'); + return 'status'; } /** - * Add action - * - * @param Request $request PSR Request - * @param Response $response PSR Response + * Name of the list route */ - public function doAdd(Request $request, Response $response): Response + protected function getListRouteName(): string { - return $this->doEdit($request, $response, null, 'add'); + return 'objectslend_statuses'; } - // /CRUD - Create - // CRUD - Read + /** + * Create empty filters + */ + protected function createFilters(): StatusList + { + return new StatusList(); + } /** - * List page + * Apply posted filters specific to the list * - * @param Request $request PSR Request - * @param Response $response PSR Response - * @param string|null $option One of 'page' or 'order' - * @param int|string|null $value Value of the option + * @param StatusList $filters Filters + * @param array $post Posted values */ - public function list(Request $request, Response $response, ?string $option = null, int|string|null $value = null): Response + protected function applyOwnFilters(Pagination $filters, array $post): void { - if (isset($this->session->objectslend_filter_statuses)) { - $filters = $this->session->objectslend_filter_statuses; - } else { - $filters = new StatusList(); - } - - if ($option !== null) { - switch ($option) { - case 'page': - $filters->current_page = (int)$value; - break; - case 'order': - $filters->orderby = $value; - break; - } + if (isset($post['stock_filter']) && is_numeric($post['stock_filter'])) { + $filters->stock_filter = $post['stock_filter']; } + } + /** + * Get list template name and parameters + * + * @param StatusList $filters Filters + * + * @return array{0: string, 1: array} + */ + protected function getListView(Pagination $filters): array + { $statuses = new Status($this->zdb, $this->preferences, $this->login, $filters); $list = $statuses->getStatusList(true); @@ -103,220 +95,85 @@ public function list(Request $request, Response $response, ?string $option = nul ); } - $this->session->objectslend_filter_statuses = $filters; - - //assign pagination variables to the template and add pagination links - $filters->setViewPagination($this->routeparser, $this->view, false); - - $lendsprefs = new Preferences($this->zdb); - // display page - $this->view->render( - $response, - $this->getTemplate('status_list'), + return [ + 'status_list', [ - 'page_title' => _T("Status list", "objectslend"), - 'require_dialog' => true, - 'statuses' => $list, - 'nb_status' => count($list), - 'olendsprefs' => $lendsprefs, - 'filters' => $filters, - 'time' => time() + 'page_title' => _T("Status list", "objectslend"), + 'statuses' => $list, + 'nb_status' => count($list) ] - ); - return $response; + ]; } /** - * Filtering + * Load an entity * - * @param Request $request PSR Request - * @param Response $response PSR Response + * @param ?int $id Entity ID, null for a new one */ - public function filter(Request $request, Response $response): Response + protected function loadEntity(?int $id): LendStatus { - $post = $request->getParsedBody(); - if (isset($this->session->objectslend_filter_statuses)) { - $filters = $this->session->objectslend_filter_statuses; - } else { - $filters = new StatusList(); - } - - //reintialize filters - if (isset($post['clear_filter'])) { - $filters->reinit(); - } else { - //string to filter - if (isset($post['filter_str'])) { //filter search string - $filters->filter_str = stripslashes( - htmlspecialchars($post['filter_str'], ENT_QUOTES) - ); - } - //activity to filter - if (isset($post['active_filter'])) { - if (is_numeric($post['active_filter'])) { - $filters->active_filter = $post['active_filter']; - } - } - //stock to filter - if (isset($post['stock_filter'])) { - if (is_numeric($post['stock_filter'])) { - $filters->stock_filter = $post['stock_filter']; - } - } - - //number of rows to show - if (isset($post['nbshow'])) { - $filters->show = $post['nbshow']; - } - } - - $this->session->objectslend_filter_statuses = $filters; - - return $response - ->withStatus(301) - ->withHeader('Location', $this->routeparser->urlFor('objectslend_statuses')); + return new LendStatus($this->zdb, $id); } - // /CRUD - Read - // CRUD - Update - /** - * Edit page + * Fill status from posted values * - * @param Request $request PSR Request - * @param Response $response PSR Response - * @param int|null $id Model id - * @param string $action Action + * @param LendStatus $entity Status + * @param array $post Posted values */ - public function edit(Request $request, Response $response, ?int $id = null, string $action = 'edit'): Response + protected function fillEntity(LendCategory|LendStatus $entity, array $post): void { - $status = new LendStatus($this->zdb, $id); - //values posted before an error - $data = $this->session->objectslend_status_data ?? null; - if (is_array($data)) { - $this->fillStatus($status, $data); - } - unset($this->session->objectslend_status_data); + $days = trim($post['rent_day_number']); + $entity + ->setText($post['text']) + ->setInStock(isset($post['in_stock'])) + ->setActive(isset($post['is_active'])) + ->setRentDayNumber(strlen($days) > 0 ? (int)$days : null); + } - if ($status->getId() !== null) { + /** + * Get edit template name and parameters + * + * @param LendStatus $entity Status + * @param string $action Either add or edit + * + * @return array{0: string, 1: array} + */ + protected function getEditView(LendCategory|LendStatus $entity, string $action): array + { + if ($entity->getId() !== null) { $title = str_replace( '%status', - $status->getText(), + $entity->getText(), _T("Edit status %status", "objectslend") ); } else { $title = _T("New status", "objectslend"); } - $params = [ - 'page_title' => $title, - 'status' => $status, - 'action' => $action + return [ + 'status_edit', + [ + 'page_title' => $title, + 'status' => $entity + ] ]; - - // display page - $this->view->render( - $response, - $this->getTemplate('status_edit'), - $params - ); - return $response; } /** - * Edit action - * - * @param Request $request PSR Request - * @param Response $response PSR Response - * @param null|int $id Model id for edit - * @param string $action Either add or edit + * Message displayed once the entity has been stored */ - public function doEdit(Request $request, Response $response, ?int $id = null, string $action = 'edit'): Response + protected function getStoredMessage(): string { - $post = $request->getParsedBody(); - $status = new LendStatus($this->zdb, $id); - $error_detected = []; - - $this->fillStatus($status, $post); - if (!$status->store()) { - $error_detected[] = _T("An error occurred while storing the status.", "objectslend"); - } - - if (count($error_detected)) { - $this->session->objectslend_status_data = $post; - foreach ($error_detected as $error) { - $this->flash->addMessage( - 'error_detected', - $error - ); - } - - $args = ($action == 'edit' ? ['id' => $status->getId()] : []); - return $response - ->withStatus(301) - ->withHeader( - 'Location', - $this->routeparser->urlFor( - 'objectslend_status_' . $action, - $args - ) - ); - } else { - //redirect to status list - $this->flash->addMessage( - 'success_detected', - _T("Status has been saved", "objectslend") - ); - - return $response - ->withStatus(301) - ->withHeader( - 'Location', - $this->routeparser->urlFor('objectslend_statuses') - ); - } + return _T("Status has been saved", "objectslend"); } /** - * Fill status from posted values - * - * @param LendStatus $status Status - * @param array $post Posted values + * Message displayed when the entity cannot be stored */ - private function fillStatus(LendStatus $status, array $post): void + protected function getStoreErrorMessage(): string { - $days = trim($post['rent_day_number']); - $status - ->setText($post['text']) - ->setInStock(isset($post['in_stock'])) - ->setActive(isset($post['is_active'])) - ->setRentDayNumber(strlen($days) > 0 ? (int)$days : null); - } - - // /CRUD - Update - // CRUD - Delete - - /** - * Get redirection URI - * - * @param array $args Route arguments - */ - public function redirectUri(array $args): string - { - return $this->routeparser->urlFor('objectslend_statuses'); - } - - /** - * Get form URI - * - * @param array $args Route arguments - */ - public function formUri(array $args): string - { - return $this->routeparser->urlFor( - 'objectslend_doremove_status', - $args - ); + return _T("An error occurred while storing the status.", "objectslend"); } /** @@ -326,25 +183,9 @@ public function formUri(array $args): string */ public function confirmRemoveTitle(array $args): string { - $status = new LendStatus($this->zdb, (int)$args['id']); return sprintf( _T('Remove status %1$s', 'objectslend'), - $status->getText() + $this->loadEntity((int)$args['id'])->getText() ); } - - /** - * Remove object - * - * @param array $args Route arguments - * @param array $post POST values - */ - protected function doDelete(array $args, array $post): bool - { - $status = new LendStatus($this->zdb, (int)$args['id']); - return $status->delete(); - } - - // /CRUD - Delete - // /CRUD } diff --git a/lib/GaletteObjectsLend/Controllers/PdfController.php b/lib/GaletteObjectsLend/Controllers/PdfController.php index 6b234848..b5b8b7ac 100644 --- a/lib/GaletteObjectsLend/Controllers/PdfController.php +++ b/lib/GaletteObjectsLend/Controllers/PdfController.php @@ -11,6 +11,7 @@ namespace GaletteObjectsLend\Controllers; use Galette\Controllers\PdfController as GPdfController; +use GaletteObjectsLend\Controllers\Crud\ObjectsController; use GaletteObjectsLend\Entity\Preferences; use GaletteObjectsLend\Filters\ObjectsList; use GaletteObjectsLend\Repository\Objects; @@ -58,11 +59,8 @@ public function printObjects(Request $request, Response $response): Response { $lendsprefs = new Preferences($this->zdb); - if (isset($this->session->objectslend_filter_objects)) { - $filters = clone $this->session->objectslend_filter_objects; - } else { - $filters = new ObjectsList(); - } + $filters = clone ($this->session->{$this->getFilterName(ObjectsController::getDefaultFilterName())} + ?? new ObjectsList()); if ($filters->orderby !== Objects::ORDERBY_CATEGORY) { $filters->orderby = Objects::ORDERBY_CATEGORY; diff --git a/tests/GaletteObjectsLend/Controllers/tests/units/CategoriesController.php b/tests/GaletteObjectsLend/Controllers/tests/units/CategoriesController.php new file mode 100644 index 00000000..292e02c7 --- /dev/null +++ b/tests/GaletteObjectsLend/Controllers/tests/units/CategoriesController.php @@ -0,0 +1,148 @@ + + */ +class CategoriesController extends GaletteRoutingTestCase +{ + protected int $seed = 20260923102218; + protected bool $load_plugins = true; + + /** + * Cleanup after each test method + */ + public function tearDown(): void + { + $this->login->logout(); + $this->zdb->execute($this->zdb->delete(LEND_PREFIX . LendCategory::TABLE)); + parent::tearDown(); + } + + /** + * Store a category + * + * @param string $name Name + */ + private function addCategory(string $name): LendCategory + { + $category = new LendCategory($this->zdb); + $category->setName($name)->setActive(true); + $this->assertTrue($category->store()); + return $category; + } + + /** + * List and filter categories + */ + public function testListAndFilter(): void + { + $this->logSuperAdmin(); + $this->addCategory('Tools '); + $this->addCategory('Books'); + + $test_response = $this->app->handle($this->createRequest(route_name: 'objectslend_categories')); + $this->assertSame(200, $test_response->getStatusCode()); + $body = (string)$test_response->getBody(); + $this->assertStringContainsString('Tools <b>', $body); + $this->assertStringContainsString('Books', $body); + + $request = $this->createRequest(route_name: 'objectslend_filter_categories', method: 'POST') + ->withParsedBody(['filter_str' => '', 'active_filter' => (string)Categories::ACTIVE]); + $test_response = $this->app->handle($request); + $this->assertSame(301, $test_response->getStatusCode()); + + $filters = $this->session->plugin_objectslend_categories_filter; + $this->assertSame('', $filters->filter_str); + $categories = new Categories($this->zdb, $this->preferences, $this->login, $filters); + $this->assertCount(1, $categories->getCategoriesList(true)); + + $test_response = $this->app->handle($this->createRequest(route_name: 'objectslend_categories')); + $body = (string)$test_response->getBody(); + $this->assertStringContainsString('Tools <b>', $body); + $this->assertStringNotContainsString('Books', $body); + } + + /** + * Add and edit a category + */ + public function testAddAndEdit(): void + { + $this->logSuperAdmin(); + $test_response = $this->app->handle($this->createRequest(route_name: 'objectslend_category_add')); + $this->assertSame(200, $test_response->getStatusCode()); + + $request = $this->createRequest(route_name: 'objectslend_category_action_add', method: 'POST') + ->withParsedBody(['name' => 'Tools', 'is_active' => '1']); + $test_response = $this->app->handle($request); + $this->assertSame(301, $test_response->getStatusCode()); + $this->assertSame( + [$this->routeparser->urlFor('objectslend_categories')], + $test_response->getHeader('Location') + ); + $this->expectFlashData(['success_detected' => ['Category has been saved']]); + + $list = (new Categories($this->zdb, $this->preferences, $this->login))->getList(true); + $this->assertCount(1, $list); + $category = $list[0]; + $this->assertSame('Tools', $category->getName(false)); + $this->assertTrue($category->isActive()); + + $test_response = $this->app->handle( + $this->createRequest(route_name: 'objectslend_category_edit', route_args: ['id' => (string)$category->getId()]) + ); + $this->assertSame(200, $test_response->getStatusCode()); + $this->assertStringContainsString('value="Tools"', (string)$test_response->getBody()); + + $request = $this->createRequest( + route_name: 'objectslend_category_action_edit', + route_args: ['id' => (string)$category->getId()], + method: 'POST' + )->withParsedBody(['name' => 'Tools (edited)']); + $this->app->handle($request); + $this->expectFlashData(['success_detected' => ['Category has been saved']]); + + $category = new LendCategory($this->zdb, $category->getId()); + $this->assertSame('Tools (edited)', $category->getName(false)); + $this->assertFalse($category->isActive()); + } + + /** + * Remove a category + */ + public function testRemove(): void + { + $this->logSuperAdmin(); + $category = $this->addCategory('To be removed'); + + $test_response = $this->app->handle( + $this->createRequest(route_name: 'objectslend_remove_category', route_args: ['id' => (string)$category->getId()]) + ); + $this->assertSame(200, $test_response->getStatusCode()); + $this->assertStringContainsString('Remove category To be removed', (string)$test_response->getBody()); + + $request = $this->createRequest( + route_name: 'objectslend_doremove_category', + route_args: ['id' => (string)$category->getId()], + method: 'POST' + )->withParsedBody(['confirm' => '1']); + $test_response = $this->app->handle($request); + $this->assertSame(301, $test_response->getStatusCode()); + $this->assertNull((new LendCategory($this->zdb, $category->getId()))->getId()); + } +} diff --git a/tests/GaletteObjectsLend/Controllers/tests/units/ObjectsController.php b/tests/GaletteObjectsLend/Controllers/tests/units/ObjectsController.php index 23cdb6d8..e5e40cbe 100644 --- a/tests/GaletteObjectsLend/Controllers/tests/units/ObjectsController.php +++ b/tests/GaletteObjectsLend/Controllers/tests/units/ObjectsController.php @@ -462,7 +462,7 @@ public function testFilterOnField(): void $test_response = $this->app->handle($request); $this->assertSame(301, $test_response->getStatusCode()); - $filters = $this->session->objectslend_filter_objects; + $filters = $this->session->plugin_objectslend_objects_filter; $this->assertSame(\GaletteObjectsLend\Repository\Objects::FILTER_SERIAL, $filters->field_filter); $objects = new \GaletteObjectsLend\Repository\Objects($this->zdb, $this->preferences, $this->login, new Preferences($this->zdb), $filters); diff --git a/tests/GaletteObjectsLend/Controllers/tests/units/StatusController.php b/tests/GaletteObjectsLend/Controllers/tests/units/StatusController.php new file mode 100644 index 00000000..d3ee8cdf --- /dev/null +++ b/tests/GaletteObjectsLend/Controllers/tests/units/StatusController.php @@ -0,0 +1,173 @@ + + */ +class StatusController extends GaletteRoutingTestCase +{ + protected int $seed = 20260923101542; + protected bool $load_plugins = true; + + /** + * Cleanup after each test method + */ + public function tearDown(): void + { + $this->login->logout(); + $this->zdb->execute($this->zdb->delete(LEND_PREFIX . LendStatus::TABLE)); + parent::tearDown(); + } + + /** + * Store a status + * + * @param string $text Text + * @param bool $in_stock In stock + */ + private function addStatus(string $text, bool $in_stock): LendStatus + { + $status = new LendStatus($this->zdb); + $status->setText($text)->setInStock($in_stock)->setActive(true); + $this->assertTrue($status->store()); + return $status; + } + + /** + * List warns when statuses are missing + */ + public function testList(): void + { + $this->logSuperAdmin(); + $this->addStatus('Shelf ', true); + + $test_response = $this->app->handle($this->createRequest(route_name: 'objectslend_statuses')); + $this->assertSame(200, $test_response->getStatusCode()); + $this->assertStringContainsString('Shelf <A>', (string)$test_response->getBody()); + $this->expectFlashData(['error_detected' => ['Please add at least one status "object borrowed"!']]); + } + + /** + * Filters are stored in session, search string as is + */ + public function testFilter(): void + { + $this->logSuperAdmin(); + $request = $this->createRequest(route_name: 'objectslend_filter_statuses', method: 'POST') + ->withParsedBody([ + 'filter_str' => ' & B', + 'active_filter' => (string)Status::ACTIVE, + 'stock_filter' => (string)Status::IN_STOCK + ]); + $test_response = $this->app->handle($request); + $this->assertSame(301, $test_response->getStatusCode()); + $this->assertSame( + [$this->routeparser->urlFor('objectslend_statuses')], + $test_response->getHeader('Location') + ); + + $filters = $this->session->plugin_objectslend_statuses_filter; + $this->assertSame(' & B', $filters->filter_str); + $this->assertSame(Status::ACTIVE, $filters->active_filter); + $this->assertSame(Status::IN_STOCK, $filters->stock_filter); + + //search string is found + $this->addStatus('Shelf & B', true); + $this->addStatus('Shelf C', true); + $statuses = new Status($this->zdb, $this->preferences, $this->login, $filters); + $this->assertCount(1, $statuses->getStatusList(true)); + + $request = $this->createRequest(route_name: 'objectslend_filter_statuses', method: 'POST') + ->withParsedBody(['clear_filter' => '1']); + $this->app->handle($request); + $this->assertNull($this->session->plugin_objectslend_statuses_filter->filter_str); + } + + /** + * Add and edit a status + */ + public function testAddAndEdit(): void + { + $this->logSuperAdmin(); + $request = $this->createRequest(route_name: 'objectslend_status_action_add', method: 'POST') + ->withParsedBody([ + 'text' => 'Garage', + 'in_stock' => '1', + 'is_active' => '1', + 'rent_day_number' => '7' + ]); + $test_response = $this->app->handle($request); + $this->assertSame(301, $test_response->getStatusCode()); + $this->assertSame( + [$this->routeparser->urlFor('objectslend_statuses')], + $test_response->getHeader('Location') + ); + $this->expectFlashData(['success_detected' => ['Status has been saved']]); + + $list = (new Status($this->zdb, $this->preferences, $this->login))->getList(true); + $this->assertCount(1, $list); + $status = $list[0]; + $this->assertSame('Garage', $status->getText()); + $this->assertTrue($status->isInStock()); + $this->assertSame(7, $status->getRentDayNumber()); + + $test_response = $this->app->handle( + $this->createRequest(route_name: 'objectslend_status_edit', route_args: ['id' => (string)$status->getId()]) + ); + $this->assertSame(200, $test_response->getStatusCode()); + $this->assertStringContainsString('value="Garage"', (string)$test_response->getBody()); + + $request = $this->createRequest( + route_name: 'objectslend_status_action_edit', + route_args: ['id' => (string)$status->getId()], + method: 'POST' + )->withParsedBody(['text' => 'Garage (edited)', 'rent_day_number' => '']); + $this->app->handle($request); + $this->expectFlashData(['success_detected' => ['Status has been saved']]); + + $status = new LendStatus($this->zdb, $status->getId()); + $this->assertSame('Garage (edited)', $status->getText()); + $this->assertFalse($status->isInStock()); + $this->assertFalse($status->isActive()); + $this->assertNull($status->getRentDayNumber()); + } + + /** + * Remove a status + */ + public function testRemove(): void + { + $this->logSuperAdmin(); + $status = $this->addStatus('To be removed', true); + + $test_response = $this->app->handle( + $this->createRequest(route_name: 'objectslend_remove_status', route_args: ['id' => (string)$status->getId()]) + ); + $this->assertSame(200, $test_response->getStatusCode()); + $this->assertStringContainsString('Remove status To be removed', (string)$test_response->getBody()); + + $request = $this->createRequest( + route_name: 'objectslend_doremove_status', + route_args: ['id' => (string)$status->getId()], + method: 'POST' + )->withParsedBody(['confirm' => '1']); + $test_response = $this->app->handle($request); + $this->assertSame(301, $test_response->getStatusCode()); + $this->assertNull((new LendStatus($this->zdb, $status->getId()))->getId()); + } +} From 0830ce9e79dffe719dc1a30f13faeaf0e33fb657 Mon Sep 17 00:00:00 2001 From: Johan Cwiklinski Date: Wed, 23 Sep 2026 10:55:55 +0200 Subject: [PATCH 4/6] Throw on storage errors, catch and log them in controllers --- .../Crud/AbstractListController.php | 38 +++++- .../Controllers/Crud/ObjectsController.php | 43 +++++-- .../Entity/LendCategory.php | 93 ++++++--------- lib/GaletteObjectsLend/Entity/LendObject.php | 105 +++++++---------- lib/GaletteObjectsLend/Entity/LendRent.php | 28 ++--- lib/GaletteObjectsLend/Entity/LendStatus.php | 109 +++++++----------- lib/GaletteObjectsLend/LendService.php | 4 +- lib/GaletteObjectsLend/Repository/Objects.php | 41 ++----- lib/GaletteObjectsLend/Repository/Rents.php | 4 +- .../tests/units/CategoriesController.php | 2 +- .../tests/units/ObjectsController.php | 14 +-- .../tests/units/StatusController.php | 27 ++++- .../Entity/tests/units/LendCategory.php | 6 +- .../Entity/tests/units/LendObject.php | 30 ++--- .../Entity/tests/units/LendRent.php | 16 +-- .../Entity/tests/units/LendStatus.php | 10 +- .../Repository/tests/units/Categories.php | 12 +- .../Repository/tests/units/Objects.php | 14 +-- .../Repository/tests/units/Status.php | 10 +- .../tests/units/LendService.php | 8 +- 20 files changed, 288 insertions(+), 326 deletions(-) diff --git a/lib/GaletteObjectsLend/Controllers/Crud/AbstractListController.php b/lib/GaletteObjectsLend/Controllers/Crud/AbstractListController.php index 6a4bd1dd..f83edca7 100644 --- a/lib/GaletteObjectsLend/Controllers/Crud/AbstractListController.php +++ b/lib/GaletteObjectsLend/Controllers/Crud/AbstractListController.php @@ -10,6 +10,7 @@ namespace GaletteObjectsLend\Controllers\Crud; +use Analog\Analog; use DI\Attribute\Inject; use Galette\Controllers\Crud\AbstractPluginController; use Galette\Core\Pagination; @@ -19,6 +20,7 @@ use GaletteObjectsLend\Filters\StatusList; use Slim\Psr7\Request; use Slim\Psr7\Response; +use Throwable; /** * Common code for categories and statuses: list, filter, edit and delete @@ -290,9 +292,11 @@ public function doEdit(Request $request, Response $response, ?int $id = null, st $entity = $this->loadEntity($id); $this->fillEntity($entity, $post); - if ($entity->store()) { + try { + $entity->store(); $errors = $this->afterStore($entity, $request, $post); - } else { + } catch (Throwable $e) { + $this->logError('store', $entity, $e); $errors = [$this->getStoreErrorMessage()]; } @@ -363,7 +367,35 @@ public function formUri(array $args): string */ protected function doDelete(array $args, array $post): bool { - return $this->loadEntity((int)$args['id'])->delete(); + $entity = $this->loadEntity((int)$args['id']); + try { + $entity->delete(); + return true; + } catch (Throwable $e) { + $this->logError('remove', $entity, $e); + return false; + } + } + + /** + * Log an error on an entity + * + * @param string $action Action that failed + * @param TEntity $entity Entity + * @param Throwable $e Error + */ + private function logError(string $action, LendCategory|LendStatus $entity, Throwable $e): void + { + Analog::log( + sprintf( + 'Unable to %1$s %2$s #%3$s | %4$s', + $action, + $this->getEntityRouteName(), + $entity->getId() ?? 'new', + $e->getMessage() + ), + Analog::ERROR + ); } // /CRUD - Delete diff --git a/lib/GaletteObjectsLend/Controllers/Crud/ObjectsController.php b/lib/GaletteObjectsLend/Controllers/Crud/ObjectsController.php index 2257d8dd..a45d6d16 100644 --- a/lib/GaletteObjectsLend/Controllers/Crud/ObjectsController.php +++ b/lib/GaletteObjectsLend/Controllers/Crud/ObjectsController.php @@ -30,6 +30,7 @@ use Galette\Repository\Members; use Slim\Psr7\Request; use Slim\Psr7\Response; +use Throwable; /** * Objects controller @@ -363,7 +364,18 @@ public function doEdit(Request $request, Response $response, ?int $id = null, st $this->fillObject($object, $post); - if ($object->store()) { + try { + $object->store(); + $stored = true; + } catch (Throwable $e) { + Analog::log( + 'Unable to store object #' . $object->getId() . ' | ' . $e->getMessage(), + Analog::ERROR + ); + $stored = false; + } + + if ($stored) { if (!empty($post['1st_status'])) { try { $this->getLendService()->changeStatus($object, (int)$post['1st_status']); @@ -381,7 +393,7 @@ public function doEdit(Request $request, Response $response, ?int $id = null, st if (!$object->getPicture()->delete()) { $error_detected[] = _T("Delete failed", "objectslend"); Analog::log( - 'Unable to delete picture for object ' . $object->getName(), + 'Unable to delete picture for object #' . $object->getId(), Analog::ERROR ); } @@ -525,7 +537,18 @@ public function doClone(Request $request, Response $response, int $id): Response { $object = new LendObject($this->zdb, $id); - if ($object->clone()) { + try { + $object->clone(); + $cloned = true; + } catch (Throwable $e) { + Analog::log( + 'Unable to clone object #' . $id . ' | ' . $e->getMessage(), + Analog::ERROR + ); + $cloned = false; + } + + if ($cloned) { $this->flash->addMessage( 'success_detected', str_replace( @@ -894,11 +917,17 @@ protected function doDelete(array $args, array $post): bool $ids = $post['id']; } - $result = $objects->removeObjects($ids); - if ($result) { - unset($this->session->{$this->getFiltersKey()}); + try { + $objects->removeObjects($ids); + } catch (Throwable $e) { + Analog::log( + 'Unable to remove objects #' . implode(', #', $ids) . ' | ' . $e->getMessage(), + Analog::ERROR + ); + return false; } - return $result; + unset($this->session->{$this->getFiltersKey()}); + return true; } // /CRUD - Delete diff --git a/lib/GaletteObjectsLend/Entity/LendCategory.php b/lib/GaletteObjectsLend/Entity/LendCategory.php index 0aa2339a..ffa26610 100644 --- a/lib/GaletteObjectsLend/Entity/LendCategory.php +++ b/lib/GaletteObjectsLend/Entity/LendCategory.php @@ -10,7 +10,6 @@ namespace GaletteObjectsLend\Entity; -use Analog\Analog; use ArrayObject; use Galette\Core\Db; use Laminas\Db\Sql\Predicate; @@ -69,19 +68,11 @@ public function __construct(Db $zdb, int|ArrayObject|null $args = null, ?array $ } if (is_int($args)) { - try { - $select = $this->zdb->select(LEND_PREFIX . self::TABLE) - ->where([self::PK => $args]); - $results = $this->zdb->execute($select); - if ($results->count() == 1) { - $this->loadFromRS($results->current()); - } - } catch (\Exception $e) { - Analog::log( - 'Something went wrong :\'( | ' . $e->getMessage() . "\n" - . $e->getTraceAsString(), - Analog::ERROR - ); + $select = $this->zdb->select(LEND_PREFIX . self::TABLE) + ->where([self::PK => $args]); + $results = $this->zdb->execute($select); + if ($results->count() == 1) { + $this->loadFromRS($results->current()); } } elseif (is_object($args)) { $this->loadFromRS($args); @@ -115,58 +106,48 @@ private function loadFromRS(ArrayObject $r): void /** * Store category */ - public function store(): bool + public function store(): void { - try { - $values = []; + $values = []; - foreach (array_keys($this->fields) as $k) { - if ($k === 'is_active' && $this->$k === false) { - //Handle booleans for postgres ; bugs #18899 and #19354 - $values[$k] = $this->zdb->isPostgres() ? 'false' : 0; - } else { - $values[$k] = $this->$k ?? null; - } + foreach (array_keys($this->fields) as $k) { + if ($k === 'is_active' && $this->$k === false) { + //Handle booleans for postgres ; bugs #18899 and #19354 + $values[$k] = $this->zdb->isPostgres() ? 'false' : 0; + } else { + $values[$k] = $this->$k ?? null; } + } - if ($this->category_id === null) { - unset($values['category_id']); - $insert = $this->zdb->insert(LEND_PREFIX . self::TABLE) - ->values($values); - $result = $this->zdb->execute($insert); - if ($result->count() > 0) { - if ($this->zdb->isPostgres()) { - /** @phpstan-ignore-next-line */ - $this->category_id = (int)$this->zdb->driver->getLastGeneratedValue( - PREFIX_DB . 'lend_category_id_seq' - ); - } else { - $this->category_id = (int)$this->zdb->driver->getLastGeneratedValue(); - } + if ($this->category_id === null) { + unset($values['category_id']); + $insert = $this->zdb->insert(LEND_PREFIX . self::TABLE) + ->values($values); + $result = $this->zdb->execute($insert); + if ($result->count() > 0) { + if ($this->zdb->isPostgres()) { + /** @phpstan-ignore-next-line */ + $this->category_id = (int)$this->zdb->driver->getLastGeneratedValue( + PREFIX_DB . 'lend_category_id_seq' + ); } else { - throw new \RuntimeException('Unable to add category!'); + $this->category_id = (int)$this->zdb->driver->getLastGeneratedValue(); } } else { - $update = $this->zdb->update(LEND_PREFIX . self::TABLE) - ->set($values) - ->where([self::PK => $this->category_id]); - $this->zdb->execute($update); + throw new \RuntimeException('Unable to add category!'); } - return true; - } catch (\Exception $e) { - Analog::log( - 'Something went wrong :\'( | ' . $e->getMessage() . "\n" - . $e->getTraceAsString(), - Analog::ERROR - ); - return false; + } else { + $update = $this->zdb->update(LEND_PREFIX . self::TABLE) + ->set($values) + ->where([self::PK => $this->category_id]); + $this->zdb->execute($update); } } /** * Drop a category. All objects for removed category will be assigned to none. */ - public function delete(): bool + public function delete(): void { $need_transaction = !$this->zdb->inTransaction(); try { @@ -190,17 +171,11 @@ public function delete(): bool if ($need_transaction) { $this->zdb->commit(); } - return true; } catch (\Exception $e) { if ($need_transaction) { $this->zdb->rollback(); } - Analog::log( - 'Something went wrong :\'( | ' . $e->getMessage() . "\n" - . $e->getTraceAsString(), - Analog::ERROR - ); - return false; + throw $e; } } diff --git a/lib/GaletteObjectsLend/Entity/LendObject.php b/lib/GaletteObjectsLend/Entity/LendObject.php index 919707e8..dc60ad71 100644 --- a/lib/GaletteObjectsLend/Entity/LendObject.php +++ b/lib/GaletteObjectsLend/Entity/LendObject.php @@ -10,7 +10,6 @@ namespace GaletteObjectsLend\Entity; -use Analog\Analog; use ArrayObject; use Galette\Core\Db; use Galette\Util\Html; @@ -82,19 +81,11 @@ class LendObject public function __construct(private Db $zdb, int|ArrayObject|null $args = null) { if (is_int($args)) { - try { - $select = $this->zdb->select(LEND_PREFIX . self::TABLE) - ->where([self::PK => $args]); - $results = $this->zdb->execute($select); - if ($results->count() == 1) { - $this->loadFromRS($results->current()); - } - } catch (\Exception $e) { - Analog::log( - 'Something went wrong :\'( | ' . $e->getMessage() . "\n" - . $e->getTraceAsString(), - Analog::ERROR - ); + $select = $this->zdb->select(LEND_PREFIX . self::TABLE) + ->where([self::PK => $args]); + $results = $this->zdb->execute($select); + if ($results->count() == 1) { + $this->loadFromRS($results->current()); } } elseif (is_object($args)) { $this->loadFromRS($args); @@ -157,62 +148,52 @@ private function loadFromRS(ArrayObject $r): void /** * Store object */ - public function store(): bool + public function store(): void { - try { - $values = []; - - foreach (array_keys($this->fields) as $k) { - if ( - ($k === 'is_active' || $k === 'price_per_day') - && $this->$k === false - ) { - //Handle booleans for postgres ; bugs #18899 and #19354 - $values[$k] = $this->zdb->isPostgres() ? 'false' : 0; - } else { - $values[$k] = $this->$k; - } + $values = []; + + foreach (array_keys($this->fields) as $k) { + if ( + ($k === 'is_active' || $k === 'price_per_day') + && $this->$k === false + ) { + //Handle booleans for postgres ; bugs #18899 and #19354 + $values[$k] = $this->zdb->isPostgres() ? 'false' : 0; + } else { + $values[$k] = $this->$k; } + } - if ($this->object_id === null) { - unset($values[self::PK]); - $insert = $this->zdb->insert(LEND_PREFIX . self::TABLE) - ->values($values); - $result = $this->zdb->execute($insert); - if ($result->count() > 0) { - if ($this->zdb->isPostgres()) { - // @phpstan-ignore arguments.count (laminas does not respect its own interfaces) - $this->object_id = (int)$this->zdb->driver->getLastGeneratedValue( - PREFIX_DB . 'lend_objects_id_seq' - ); - } else { - $this->object_id = (int)$this->zdb->driver->getLastGeneratedValue(); - } - $this->picture = null; + if ($this->object_id === null) { + unset($values[self::PK]); + $insert = $this->zdb->insert(LEND_PREFIX . self::TABLE) + ->values($values); + $result = $this->zdb->execute($insert); + if ($result->count() > 0) { + if ($this->zdb->isPostgres()) { + // @phpstan-ignore arguments.count (laminas does not respect its own interfaces) + $this->object_id = (int)$this->zdb->driver->getLastGeneratedValue( + PREFIX_DB . 'lend_objects_id_seq' + ); } else { - throw new \Exception(_T("Object has not been added :(", "objectslend")); + $this->object_id = (int)$this->zdb->driver->getLastGeneratedValue(); } + $this->picture = null; } else { - $update = $this->zdb->update(LEND_PREFIX . self::TABLE) - ->set($values) - ->where([self::PK => $this->object_id]); - $this->zdb->execute($update); + throw new \Exception(_T("Object has not been added :(", "objectslend")); } - return true; - } catch (\Exception $e) { - Analog::log( - 'Something went wrong :\'( | ' . $e->getMessage() . "\n" - . $e->getTraceAsString(), - Analog::ERROR - ); - throw $e; + } else { + $update = $this->zdb->update(LEND_PREFIX . self::TABLE) + ->set($values) + ->where([self::PK => $this->object_id]); + $this->zdb->execute($update); } } /** * Delete object */ - public function delete(): bool + public function delete(): void { $need_transaction = !$this->zdb->inTransaction(); try { @@ -233,16 +214,10 @@ public function delete(): bool if ($need_transaction) { $this->zdb->commit(); } - return true; } catch (\Exception $e) { if ($need_transaction) { $this->zdb->rollback(); } - Analog::log( - 'Something went wrong :\'( | ' . $e->getMessage() . "\n" - . $e->getTraceAsString(), - Analog::ERROR - ); throw $e; } } @@ -252,12 +227,12 @@ public function delete(): bool * * The copy has neither picture nor rents. */ - public function clone(): bool + public function clone(): void { $this->object_id = null; $this->rent_id = null; $this->picture = null; - return $this->store(); + $this->store(); } /** diff --git a/lib/GaletteObjectsLend/Entity/LendRent.php b/lib/GaletteObjectsLend/Entity/LendRent.php index 7786f4e8..3f0dd6e5 100644 --- a/lib/GaletteObjectsLend/Entity/LendRent.php +++ b/lib/GaletteObjectsLend/Entity/LendRent.php @@ -62,19 +62,11 @@ public function __construct(private Db $zdb, int|ArrayObject|null $args = null) $this->date_begin = date('Y-m-d H:i:s'); if (is_int($args)) { - try { - $select = $this->zdb->select(LEND_PREFIX . self::TABLE) - ->where([self::PK => $args]); - $result = $this->zdb->execute($select); - if ($result->count() == 1) { - $this->loadFromRS($result->current()); - } - } catch (\Exception $e) { - Analog::log( - 'Something went wrong :\'( | ' . $e->getMessage() . "\n" - . $e->getTraceAsString(), - Analog::ERROR - ); + $select = $this->zdb->select(LEND_PREFIX . self::TABLE) + ->where([self::PK => $args]); + $result = $this->zdb->execute($select); + if ($result->count() == 1) { + $this->loadFromRS($result->current()); } } elseif (is_object($args)) { $this->loadFromRS($args); @@ -114,7 +106,7 @@ private function loadFromRS(ArrayObject $r): void /** * Store current element */ - public function store(): bool + public function store(): void { $need_transaction = !$this->zdb->inTransaction(); try { @@ -157,17 +149,11 @@ public function store(): bool if ($need_transaction) { $this->zdb->commit(); } - return true; } catch (\Exception $e) { if ($need_transaction) { $this->zdb->rollback(); } - Analog::log( - 'Something went wrong :\'( | ' . $e->getMessage() . "\n" - . $e->getTraceAsString(), - Analog::ERROR - ); - return false; + throw $e; } } diff --git a/lib/GaletteObjectsLend/Entity/LendStatus.php b/lib/GaletteObjectsLend/Entity/LendStatus.php index 1d8406e2..2f877768 100644 --- a/lib/GaletteObjectsLend/Entity/LendStatus.php +++ b/lib/GaletteObjectsLend/Entity/LendStatus.php @@ -10,7 +10,6 @@ namespace GaletteObjectsLend\Entity; -use Analog\Analog; use ArrayObject; use Galette\Core\Db; @@ -52,19 +51,11 @@ public function __construct(Db $zdb, int|ArrayObject|null $args = null) $this->zdb = $zdb; if (is_int($args)) { - try { - $select = $this->zdb->select(LEND_PREFIX . self::TABLE) - ->where([self::PK => $args]); - $result = $this->zdb->execute($select); - if ($result->count() == 1) { - $this->loadFromRS($result->current()); - } - } catch (\Exception $e) { - Analog::log( - 'Something went wrong :\'( | ' . $e->getMessage() . "\n" - . $e->getTraceAsString(), - Analog::ERROR - ); + $select = $this->zdb->select(LEND_PREFIX . self::TABLE) + ->where([self::PK => $args]); + $result = $this->zdb->execute($select); + if ($result->count() == 1) { + $this->loadFromRS($result->current()); } } elseif (is_object($args)) { $this->loadFromRS($args); @@ -88,75 +79,55 @@ private function loadFromRS(ArrayObject $r): void /** * Store current element */ - public function store(): bool + public function store(): void { - try { - $values = []; - - foreach (array_keys($this->fields) as $k) { - if ( - ($k === 'is_active' || $k === 'in_stock') - && $this->$k === false - ) { - //Handle booleans for postgres ; bugs #18899 and #19354 - $values[$k] = $this->zdb->isPostgres() ? 'false' : 0; - } else { - $values[$k] = $this->$k ?? null; - } + $values = []; + + foreach (array_keys($this->fields) as $k) { + if ( + ($k === 'is_active' || $k === 'in_stock') + && $this->$k === false + ) { + //Handle booleans for postgres ; bugs #18899 and #19354 + $values[$k] = $this->zdb->isPostgres() ? 'false' : 0; + } else { + $values[$k] = $this->$k ?? null; } + } - if ($this->status_id === null) { - unset($values[self::PK]); - $insert = $this->zdb->insert(LEND_PREFIX . self::TABLE) - ->values($values); - $result = $this->zdb->execute($insert); - if ($result->count() > 0) { - if ($this->zdb->isPostgres()) { - /** @phpstan-ignore-next-line */ - $this->status_id = (int)$this->zdb->driver->getLastGeneratedValue( - PREFIX_DB . 'lend_status_id_seq' - ); - } else { - $this->status_id = (int)$this->zdb->driver->getLastGeneratedValue(); - } + if ($this->status_id === null) { + unset($values[self::PK]); + $insert = $this->zdb->insert(LEND_PREFIX . self::TABLE) + ->values($values); + $result = $this->zdb->execute($insert); + if ($result->count() > 0) { + if ($this->zdb->isPostgres()) { + /** @phpstan-ignore-next-line */ + $this->status_id = (int)$this->zdb->driver->getLastGeneratedValue( + PREFIX_DB . 'lend_status_id_seq' + ); } else { - throw new \Exception(_T("Status has not been added :(", "objectslend")); + $this->status_id = (int)$this->zdb->driver->getLastGeneratedValue(); } } else { - $update = $this->zdb->update(LEND_PREFIX . self::TABLE) - ->set($values) - ->where([self::PK => $this->status_id]); - $this->zdb->execute($update); + throw new \Exception(_T("Status has not been added :(", "objectslend")); } - return true; - } catch (\Exception $e) { - Analog::log( - 'Something went wrong :\'( | ' . $e->getMessage() . "\n" - . $e->getTraceAsString(), - Analog::ERROR - ); - return false; + } else { + $update = $this->zdb->update(LEND_PREFIX . self::TABLE) + ->set($values) + ->where([self::PK => $this->status_id]); + $this->zdb->execute($update); } } /** * Delete status */ - public function delete(): bool + public function delete(): void { - try { - $delete = $this->zdb->delete(LEND_PREFIX . self::TABLE) - ->where([self::PK => $this->status_id]); - $this->zdb->execute($delete); - return true; - } catch (\Exception $e) { - Analog::log( - 'Something went wrong :\'( | ' . $e->getMessage() . "\n" - . $e->getTraceAsString(), - Analog::ERROR - ); - return false; - } + $delete = $this->zdb->delete(LEND_PREFIX . self::TABLE) + ->where([self::PK => $this->status_id]); + $this->zdb->execute($delete); } /** diff --git a/lib/GaletteObjectsLend/LendService.php b/lib/GaletteObjectsLend/LendService.php index 489372d4..8730d29b 100644 --- a/lib/GaletteObjectsLend/LendService.php +++ b/lib/GaletteObjectsLend/LendService.php @@ -285,9 +285,7 @@ private function openRent( if ($date_forecast !== null) { $rent->setDateForecast($date_forecast); } - if (!$rent->store()) { - throw new \RuntimeException('Unable to store rent for object #' . $object_id); - } + $rent->store(); $update = $this->zdb->update(LEND_PREFIX . LendObject::TABLE) ->set([LendRent::PK => $rent->getId()]) diff --git a/lib/GaletteObjectsLend/Repository/Objects.php b/lib/GaletteObjectsLend/Repository/Objects.php index cbf14567..45b8bd75 100644 --- a/lib/GaletteObjectsLend/Repository/Objects.php +++ b/lib/GaletteObjectsLend/Repository/Objects.php @@ -10,7 +10,6 @@ namespace GaletteObjectsLend\Repository; -use Analog\Analog; use ArrayObject; use Galette\Core\Db; use Galette\Core\Login; @@ -112,7 +111,7 @@ public function getList(bool $as_objects = false): array|ResultSet * * @param array $ids Objects identifiers to delete */ - public function removeObjects(array $ids): bool + public function removeObjects(array $ids): void { $need_transaction = !$this->zdb->inTransaction(); try { @@ -122,52 +121,26 @@ public function removeObjects(array $ids): bool $update = $this->zdb->update(LEND_PREFIX . self::TABLE); $update->set(['rent_id' => null]); - $update->where->in( - self::PK, - $ids - ); + $update->where->in(self::PK, $ids); $this->zdb->execute($update); $delete = $this->zdb->delete(LEND_PREFIX . LendRent::TABLE); - $delete->where->in( - self::PK, - $ids - ); + $delete->where->in(self::PK, $ids); $this->zdb->execute($delete); $delete = $this->zdb->delete(LEND_PREFIX . self::TABLE); - $delete->where->in( - self::PK, - $ids - ); + $delete->where->in(self::PK, $ids); $this->zdb->execute($delete); + if ($need_transaction) { $this->zdb->commit(); } - return true; - } catch (\Exception $e) { + } catch (\Throwable $e) { if ($need_transaction) { $this->zdb->rollback(); } - - if ($this->zdb->isForeignKeyException($e)) { - Analog::log( - 'Object mays still have existing dependencies in the ' - . 'database.' - . 'Please remove dependencies before trying ' - . 'to remove it.', - Analog::ERROR - ); - } else { - Analog::log( - 'Unable to delete selected object(s) |' - . $e->getMessage(), - Analog::ERROR - ); - } + throw $e; } - - return false; } /** diff --git a/lib/GaletteObjectsLend/Repository/Rents.php b/lib/GaletteObjectsLend/Repository/Rents.php index e0a3b382..ca19deb6 100644 --- a/lib/GaletteObjectsLend/Repository/Rents.php +++ b/lib/GaletteObjectsLend/Repository/Rents.php @@ -89,9 +89,7 @@ public function closeAllForObject(int $object_id, string $comments): void $rent ->setDateEnd(date('Y-m-d H:i:s')) ->setComments($comments); //FIXME: will replace any existing comments :/ - if (!$rent->store()) { - throw new \RuntimeException('Unable to close rent #' . $rent->getId()); - } + $rent->store(); } } } diff --git a/tests/GaletteObjectsLend/Controllers/tests/units/CategoriesController.php b/tests/GaletteObjectsLend/Controllers/tests/units/CategoriesController.php index 292e02c7..d19b1845 100644 --- a/tests/GaletteObjectsLend/Controllers/tests/units/CategoriesController.php +++ b/tests/GaletteObjectsLend/Controllers/tests/units/CategoriesController.php @@ -43,7 +43,7 @@ private function addCategory(string $name): LendCategory { $category = new LendCategory($this->zdb); $category->setName($name)->setActive(true); - $this->assertTrue($category->store()); + $category->store(); return $category; } diff --git a/tests/GaletteObjectsLend/Controllers/tests/units/ObjectsController.php b/tests/GaletteObjectsLend/Controllers/tests/units/ObjectsController.php index e5e40cbe..86dc26fc 100644 --- a/tests/GaletteObjectsLend/Controllers/tests/units/ObjectsController.php +++ b/tests/GaletteObjectsLend/Controllers/tests/units/ObjectsController.php @@ -47,19 +47,19 @@ public function setUp(): void $status->setText('In stock'); $status->setInStock(true); $status->setActive(true); - $this->assertTrue($status->store()); + $status->store(); $this->instock_status = $status->getId(); $status = new LendStatus($this->zdb); $status->setText('Lent'); $status->setInStock(false); $status->setActive(true); - $this->assertTrue($status->store()); + $status->store(); $this->lent_status = $status->getId(); $object = new LendObject($this->zdb); $object->setName('Test object'); - $this->assertTrue($object->store()); + $object->store(); $this->object_id = $object->getId(); } @@ -170,7 +170,7 @@ private function lendObject(int $member_id): void */ private function storeCurrentRent(LendRent $rent): void { - $this->assertTrue($rent->store()); + $rent->store(); $update = $this->zdb->update(LEND_PREFIX . LendObject::TABLE) ->set([LendRent::PK => $rent->getId()]) ->where([LendObject::PK => $this->object_id]); @@ -448,7 +448,7 @@ public function testFilterOnField(): void { $object = new LendObject($this->zdb, $this->object_id); $object->setSerialNumber('SN-4242'); - $this->assertTrue($object->store()); + $object->store(); $this->logSuperAdmin(); $request = $this->createRequest( @@ -617,7 +617,7 @@ public function testTakeInactiveObject(): void { $object = new LendObject($this->zdb, $this->object_id); $object->setActive(false); - $this->assertTrue($object->store()); + $object->store(); $this->setPrefs(false); $this->logSuperAdmin(); @@ -688,7 +688,7 @@ private function setRentPrice(float $rent_price): void $object = new LendObject($this->zdb, $this->object_id); $object->setRentPrice($rent_price); $object->setSerialNumber('SN-42'); - $this->assertTrue($object->store()); + $object->store(); } /** diff --git a/tests/GaletteObjectsLend/Controllers/tests/units/StatusController.php b/tests/GaletteObjectsLend/Controllers/tests/units/StatusController.php index d3ee8cdf..29484f62 100644 --- a/tests/GaletteObjectsLend/Controllers/tests/units/StatusController.php +++ b/tests/GaletteObjectsLend/Controllers/tests/units/StatusController.php @@ -10,6 +10,7 @@ namespace GaletteObjectsLend\Controllers\tests\units; +use Analog\Analog; use Galette\Tests\GaletteRoutingTestCase; use GaletteObjectsLend\Entity\LendStatus; use GaletteObjectsLend\Repository\Status; @@ -23,6 +24,8 @@ class StatusController extends GaletteRoutingTestCase { protected int $seed = 20260923101542; protected bool $load_plugins = true; + //a failing query aborts the whole transaction on PostgreSQL + protected bool $db_transactions = false; /** * Cleanup after each test method @@ -44,7 +47,7 @@ private function addStatus(string $text, bool $in_stock): LendStatus { $status = new LendStatus($this->zdb); $status->setText($text)->setInStock($in_stock)->setActive(true); - $this->assertTrue($status->store()); + $status->store(); return $status; } @@ -147,6 +150,28 @@ public function testAddAndEdit(): void $this->assertNull($status->getRentDayNumber()); } + /** + * A status that cannot be stored brings the form back with posted values + */ + public function testStoreError(): void + { + $this->logSuperAdmin(); + $text = str_repeat('x', 150); + $request = $this->createRequest(route_name: 'objectslend_status_action_add', method: 'POST') + ->withParsedBody(['text' => $text, 'rent_day_number' => '']); + $test_response = $this->app->handle($request); + $this->assertSame(301, $test_response->getStatusCode()); + $this->assertSame( + [$this->routeparser->urlFor('objectslend_status_add')], + $test_response->getHeader('Location') + ); + $this->expectFlashData(['error_detected' => ['An error occurred while storing the status.']]); + $this->expectLogEntry(Analog::ERROR, 'Unable to store status #new'); + $this->expectLogEntry(Analog::ERROR, 'Query error'); + $this->assertSame($text, $this->session->objectslend_status_data['text']); + $this->assertCount(0, (new Status($this->zdb, $this->preferences, $this->login))->getList(true)); + } + /** * Remove a status */ diff --git a/tests/GaletteObjectsLend/Entity/tests/units/LendCategory.php b/tests/GaletteObjectsLend/Entity/tests/units/LendCategory.php index 7e6b93bc..ef95eec2 100644 --- a/tests/GaletteObjectsLend/Entity/tests/units/LendCategory.php +++ b/tests/GaletteObjectsLend/Entity/tests/units/LendCategory.php @@ -63,7 +63,7 @@ public function testCrud(): void $category->setName('Test category'); $category->setActive(false); - $this->assertTrue($category->store()); + $category->store(); $cid = $category->getId(); $this->assertGreaterThan(0, $cid); @@ -72,12 +72,12 @@ public function testCrud(): void $this->assertFalse($category->isActive()); $category->setName('Test category (edited)'); - $this->assertTrue($category->store()); + $category->store(); $category = new \GaletteObjectsLend\Entity\LendCategory($this->zdb, $cid); $this->assertSame('Test category (edited) (0)', $category->getName()); - $this->assertTrue($category->delete()); + $category->delete(); new \GaletteObjectsLend\Entity\LendCategory($this->zdb, $cid); } } diff --git a/tests/GaletteObjectsLend/Entity/tests/units/LendObject.php b/tests/GaletteObjectsLend/Entity/tests/units/LendObject.php index 67d3018e..2a09ce8c 100644 --- a/tests/GaletteObjectsLend/Entity/tests/units/LendObject.php +++ b/tests/GaletteObjectsLend/Entity/tests/units/LendObject.php @@ -105,7 +105,7 @@ public function testCrud(): void $object->setPricePerDay(true); $object->setWeight(186.00); - $this->assertTrue($object->store()); + $object->store(); $oid = $object->getId(); $this->assertGreaterThan(0, $oid); @@ -117,7 +117,7 @@ public function testCrud(): void $rent->setDateEnd($edate->format('Y-m-d H:i:s')); $rent->setStatusId($this->active_instock_status); $rent->setObjectId($oid); - $this->assertTrue($rent->store()); + $rent->store(); //current rent is set by LendService $update = $this->zdb->update(LEND_PREFIX . \GaletteObjectsLend\Entity\LendObject::TABLE) ->set([\GaletteObjectsLend\Entity\LendRent::PK => $rent->getId()]) @@ -143,7 +143,7 @@ public function testCrud(): void $object->setDescription('An object description'); $object->setSerialNumber('SE-aBc-RI@L'); $object->setDimension('10x50'); - $this->assertTrue($object->store()); + $object->store(); $object = $objects->getWithCurrentRent($oid); $this->assertSame('An object (edited)', $object->getName()); @@ -153,7 +153,7 @@ public function testCrud(): void //edit category to inactive one $object->setCategoryId($this->inactive_category_id); - $this->assertTrue($object->store()); + $object->store(); $object = $objects->getWithCurrentRent($oid); $this->assertFalse($object->isActive()); $this->assertTrue($object->isObjectActive()); @@ -164,29 +164,29 @@ public function testCrud(): void $rm_category->setName('Category to be removed'); $rm_category->setActive(true); - $this->assertTrue($rm_category->store()); + $rm_category->store(); $category_id = $rm_category->getId(); $object->setCategoryId($category_id); - $this->assertTrue($object->store()); + $object->store(); $object = $objects->getWithCurrentRent($oid); $this->assertSame($category_id, $object->getCategoryId()); - $this->assertTrue($rm_category->delete()); + $rm_category->delete(); $object = $objects->getWithCurrentRent($oid); $this->assertNull($object->getCategoryId()); //clone - $this->assertTrue($object->clone()); + $object->clone(); $clone_id = $object->getId(); $this->assertNotEquals($oid, $clone_id); $this->assertSame('An object (edited)', $object->getName()); $object = new \GaletteObjectsLend\Entity\LendObject($this->zdb, $oid); - $this->assertTrue($object->delete()); + $object->delete(); $object = new \GaletteObjectsLend\Entity\LendObject($this->zdb, $clone_id); - $this->assertTrue($object->delete()); + $object->delete(); } /** @@ -198,20 +198,20 @@ private function createStatus(): void $status->setText('One active status'); $status->setInStock(true); $status->setActive(true); - $this->assertTrue($status->store()); + $status->store(); $this->active_instock_status = $status->getId(); $status = new \GaletteObjectsLend\Entity\LendStatus($this->zdb); $status->setText('Another active status'); $status->setInStock(false); $status->setActive(true); - $this->assertTrue($status->store()); + $status->store(); $status = new \GaletteObjectsLend\Entity\LendStatus($this->zdb); $status->setText('One inactive status'); $status->setInStock(true); $status->setActive(false); - $this->assertTrue($status->store()); + $status->store(); } /** @@ -224,7 +224,7 @@ private function createCategories(): void $category->setName('Active test category'); $category->setActive(true); - $this->assertTrue($category->store()); + $category->store(); $this->active_category_id = $category->getId(); $this->assertGreaterThan(0, $this->active_category_id); @@ -233,7 +233,7 @@ private function createCategories(): void $category->setName('Inactive test category'); $category->setActive(false); - $this->assertTrue($category->store()); + $category->store(); $this->inactive_category_id = $category->getId(); $this->assertGreaterThan(0, $this->inactive_category_id); } diff --git a/tests/GaletteObjectsLend/Entity/tests/units/LendRent.php b/tests/GaletteObjectsLend/Entity/tests/units/LendRent.php index d0567d9f..98d4c66d 100644 --- a/tests/GaletteObjectsLend/Entity/tests/units/LendRent.php +++ b/tests/GaletteObjectsLend/Entity/tests/units/LendRent.php @@ -79,7 +79,7 @@ public function testCrud(): void $object = new \GaletteObjectsLend\Entity\LendObject($this->zdb); $object->setName('Test object'); - $this->assertTrue($object->store()); + $object->store(); $oid = $object->getId(); $bdate = new \DateTime('2024-05-22 19:46:21'); @@ -87,7 +87,7 @@ public function testCrud(): void $rent->setObjectId($oid); $rent->setStatusId($this->active_instock_status); $rent->setComments('Test comment'); - $this->assertTrue($rent->store()); + $rent->store(); $rent_id = $rent->getId(); $rent = new \GaletteObjectsLend\Entity\LendRent($this->zdb, $rent_id); @@ -98,7 +98,7 @@ public function testCrud(): void $rent = new \GaletteObjectsLend\Entity\LendRent($this->zdb, $rent_id); $rent->setStatusId($this->active_notinstock_status); $rent->setAdherentId($member->id); - $this->assertTrue($rent->store()); + $rent->store(); $rent = new \GaletteObjectsLend\Entity\LendRent($this->zdb, $rent_id); $this->assertSame($this->active_notinstock_status, $rent->getStatusId()); @@ -116,7 +116,7 @@ public function testCrud(): void $rent->setObjectId($oid); $rent->setStatusId($this->active_instock_status); $rent->setComments('Test 2 comment'); - $this->assertTrue($rent->store()); + $rent->store(); $rent2_id = $rent->getId(); $this->assertNotEquals($rent_id, $rent2_id); $this->assertSame($rent2_edate->format('Y-m-d'), $rent->getDateForecast()); @@ -140,7 +140,7 @@ public function testCrud(): void $this->assertSame('Test 2 comment', $rent->getComments()); //cleanup to avoid constraint errors - $this->assertTrue($object->delete()); + $object->delete(); /*$status = new \GaletteObjectsLend\Entity\LendStatus($this->zdb); $status->setText('One active status'); @@ -198,20 +198,20 @@ private function createStatus(): void $status->setText('One active status'); $status->setInStock(true); $status->setActive(true); - $this->assertTrue($status->store()); + $status->store(); $this->active_instock_status = $status->getId(); $status = new \GaletteObjectsLend\Entity\LendStatus($this->zdb); $status->setText('Another active status'); $status->setInStock(false); $status->setActive(true); - $this->assertTrue($status->store()); + $status->store(); $this->active_notinstock_status = $status->getId(); $status = new \GaletteObjectsLend\Entity\LendStatus($this->zdb); $status->setText('One inactive status'); $status->setInStock(true); $status->setActive(false); - $this->assertTrue($status->store()); + $status->store(); } } diff --git a/tests/GaletteObjectsLend/Entity/tests/units/LendStatus.php b/tests/GaletteObjectsLend/Entity/tests/units/LendStatus.php index f1874a55..6f4c978d 100644 --- a/tests/GaletteObjectsLend/Entity/tests/units/LendStatus.php +++ b/tests/GaletteObjectsLend/Entity/tests/units/LendStatus.php @@ -64,21 +64,21 @@ public function testCrud(): void $status->setText('One active status'); $status->setInStock(true); $status->setActive(true); - $this->assertTrue($status->store()); + $status->store(); $status_one = $status->getId(); $status = new \GaletteObjectsLend\Entity\LendStatus($this->zdb); $status->setText('Another active status'); $status->setInStock(false); $status->setActive(true); - $this->assertTrue($status->store()); + $status->store(); $status_two = $status->getId(); $status = new \GaletteObjectsLend\Entity\LendStatus($this->zdb); $status->setText('One inactive status'); $status->setInStock(true); $status->setActive(false); - $this->assertTrue($status->store()); + $status->store(); $list = (new \GaletteObjectsLend\Repository\Status($this->zdb, $this->preferences, $this->login))->getActiveTakeAwayStatuses(); $this->assertCount(1, $list); @@ -96,12 +96,12 @@ public function testCrud(): void $status = new \GaletteObjectsLend\Entity\LendStatus($this->zdb, $status_one); $status->setText('One active status (edited)'); - $this->assertTrue($status->store()); + $status->store(); $status = new \GaletteObjectsLend\Entity\LendStatus($this->zdb, $status_one); $this->assertSame('One active status (edited)', $status->getText()); - $this->assertTrue($status->delete()); + $status->delete(); $status = new \GaletteObjectsLend\Entity\LendStatus($this->zdb, $status_one); $this->assertNull($status->getId()); } diff --git a/tests/GaletteObjectsLend/Repository/tests/units/Categories.php b/tests/GaletteObjectsLend/Repository/tests/units/Categories.php index 9bec8257..d01ead69 100644 --- a/tests/GaletteObjectsLend/Repository/tests/units/Categories.php +++ b/tests/GaletteObjectsLend/Repository/tests/units/Categories.php @@ -53,19 +53,19 @@ public function testGetList(): void $category = new \GaletteObjectsLend\Entity\LendCategory($this->zdb); $category->setName('One category'); $category->setActive(true); - $this->assertTrue($category->store()); + $category->store(); $cat_one_id = $category->getId(); $category = new \GaletteObjectsLend\Entity\LendCategory($this->zdb); $category->setName('Another category'); $category->setActive(true); - $this->assertTrue($category->store()); + $category->store(); $cat_two_id = $category->getId(); $category = new \GaletteObjectsLend\Entity\LendCategory($this->zdb); $category->setName('Yet another category'); $category->setActive(false); - $this->assertTrue($category->store()); + $category->store(); $filters = new \GaletteObjectsLend\Filters\CategoriesList(); $categories = new \GaletteObjectsLend\Repository\Categories($this->zdb, $this->preferences, $this->login, $filters); @@ -89,7 +89,7 @@ public function testGetList(): void $object = new \GaletteObjectsLend\Entity\LendObject($this->zdb); $object->setName('One object'); $object->setCategoryId($cat_one_id); - $this->assertTrue($object->store()); + $object->store(); $this->assertCount(1, $categories->getCategoriesList(true)); @@ -107,7 +107,7 @@ public function testGetList(): void $object = new \GaletteObjectsLend\Entity\LendObject($this->zdb); $object->setName('Another object'); $object->setCategoryId($cat_one_id); - $this->assertTrue($object->store()); + $object->store(); $filters->reinit(); $this->assertCount(3, $categories->getCategoriesList(true)); @@ -117,7 +117,7 @@ public function testGetList(): void $object = new \GaletteObjectsLend\Entity\LendObject($this->zdb); $object->setName('Object in another category'); $object->setCategoryId($cat_two_id); - $this->assertTrue($object->store()); + $object->store(); $filters->not_empty = true; $this->assertCount(2, $categories->getCategoriesList(true)); diff --git a/tests/GaletteObjectsLend/Repository/tests/units/Objects.php b/tests/GaletteObjectsLend/Repository/tests/units/Objects.php index 888b9f6e..2931298f 100644 --- a/tests/GaletteObjectsLend/Repository/tests/units/Objects.php +++ b/tests/GaletteObjectsLend/Repository/tests/units/Objects.php @@ -65,14 +65,14 @@ public function testGetList(): void $category = new \GaletteObjectsLend\Entity\LendCategory($this->zdb); $category->setName('First category'); $category->setActive(true); - $this->assertTrue($category->store()); + $category->store(); $first_category_id = $category->getId(); $this->assertGreaterThan(0, $first_category_id); $category = new \GaletteObjectsLend\Entity\LendCategory($this->zdb); $category->setName('Second category'); $category->setActive(true); - $this->assertTrue($category->store()); + $category->store(); $second_category_id = $category->getId(); $this->assertGreaterThan(0, $second_category_id); @@ -80,7 +80,7 @@ public function testGetList(): void $object->setName('First object'); $object->setCategoryId($first_category_id); $object->setActive(true); - $this->assertTrue($object->store()); + $object->store(); $first_object_id = $object->getId(); $object = new \GaletteObjectsLend\Entity\LendObject($this->zdb); @@ -88,14 +88,14 @@ public function testGetList(): void $object->setDescription('First description'); $object->setCategoryId($first_category_id); $object->setActive(true); - $this->assertTrue($object->store()); + $object->store(); $second_object_id = $object->getId(); $object = new \GaletteObjectsLend\Entity\LendObject($this->zdb); $object->setName('Third object'); $object->setCategoryId($second_category_id); $object->setActive(true); - $this->assertTrue($object->store()); + $object->store(); $third_object_id = $object->getId(); $object = new \GaletteObjectsLend\Entity\LendObject($this->zdb); @@ -103,7 +103,7 @@ public function testGetList(): void $object->setSerialNumber('GGABCDEXX'); $object->setDimension('210x297'); $object->setActive(false); - $this->assertTrue($object->store()); + $object->store(); //ids are not reset between tests, a hardcoded one may exist $missing_id = $object->getId() + 1; @@ -217,7 +217,7 @@ public function testGetList(): void $filters->orderby = \GaletteObjectsLend\Repository\Objects::ORDERBY_CATEGORY; $this->assertCount(4, $objects->getObjectsList(true)); - $this->assertTrue($objects->removeObjects([$first_object_id, $second_object_id, $third_object_id])); + $objects->removeObjects([$first_object_id, $second_object_id, $third_object_id]); $this->assertCount(1, $objects->getObjectsList(true)); } } diff --git a/tests/GaletteObjectsLend/Repository/tests/units/Status.php b/tests/GaletteObjectsLend/Repository/tests/units/Status.php index 7f8aeaf3..1204b910 100644 --- a/tests/GaletteObjectsLend/Repository/tests/units/Status.php +++ b/tests/GaletteObjectsLend/Repository/tests/units/Status.php @@ -63,31 +63,31 @@ public function testGetList(): void $status->setText('One active in stock status'); $status->setInStock(true); $status->setActive(true); - $this->assertTrue($status->store()); + $status->store(); $status = new \GaletteObjectsLend\Entity\LendStatus($this->zdb); $status->setText('One active in stock status'); $status->setInStock(true); $status->setActive(true); - $this->assertTrue($status->store()); + $status->store(); $status = new \GaletteObjectsLend\Entity\LendStatus($this->zdb); $status->setText('One active not in stock status'); $status->setInStock(false); $status->setActive(true); - $this->assertTrue($status->store()); + $status->store(); $status = new \GaletteObjectsLend\Entity\LendStatus($this->zdb); $status->setText('One inactive in stock status'); $status->setInStock(true); $status->setActive(false); - $this->assertTrue($status->store()); + $status->store(); $status = new \GaletteObjectsLend\Entity\LendStatus($this->zdb); $status->setText('One inactive not in stock status'); $status->setInStock(false); $status->setActive(false); - $this->assertTrue($status->store()); + $status->store(); $filters = new \GaletteObjectsLend\Filters\StatusList(); $status = new \GaletteObjectsLend\Repository\Status($this->zdb, $this->preferences, $this->login, $filters); diff --git a/tests/GaletteObjectsLend/tests/units/LendService.php b/tests/GaletteObjectsLend/tests/units/LendService.php index 0291f9a6..daf44a04 100644 --- a/tests/GaletteObjectsLend/tests/units/LendService.php +++ b/tests/GaletteObjectsLend/tests/units/LendService.php @@ -56,20 +56,20 @@ public function setUp(): void $status->setText('In stock'); $status->setInStock(true); $status->setActive(true); - $this->assertTrue($status->store()); + $status->store(); $this->instock_status = $status->getId(); $status = new LendStatus($this->zdb); $status->setText('Lent'); $status->setInStock(false); $status->setActive(true); - $this->assertTrue($status->store()); + $status->store(); $this->lent_status = $status->getId(); $object = new LendObject($this->zdb); $object->setName('Service object'); $object->setRentPrice(3.5); - $this->assertTrue($object->store()); + $object->store(); $this->object_id = $object->getId(); } @@ -185,7 +185,7 @@ public function testChangeStatusInvalid(): void $status->setText('Inactive'); $status->setInStock(true); $status->setActive(false); - $this->assertTrue($status->store()); + $status->store(); $this->logSuperAdmin(); $service = $this->getService(); From 56dabd921ec9ebc6da87d80a790989ea95c8e663 Mon Sep 17 00:00:00 2001 From: Johan Cwiklinski Date: Wed, 23 Sep 2026 22:40:14 +0200 Subject: [PATCH 5/6] Do not rely on example statuses being removed in status controller tests --- .../Controllers/tests/units/StatusController.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/GaletteObjectsLend/Controllers/tests/units/StatusController.php b/tests/GaletteObjectsLend/Controllers/tests/units/StatusController.php index 29484f62..ee63ea5a 100644 --- a/tests/GaletteObjectsLend/Controllers/tests/units/StatusController.php +++ b/tests/GaletteObjectsLend/Controllers/tests/units/StatusController.php @@ -27,6 +27,16 @@ class StatusController extends GaletteRoutingTestCase //a failing query aborts the whole transaction on PostgreSQL protected bool $db_transactions = false; + /** + * Set up tests + */ + public function setUp(): void + { + parent::setUp(); + //installation scripts add example statuses + $this->zdb->execute($this->zdb->delete(LEND_PREFIX . LendStatus::TABLE)); + } + /** * Cleanup after each test method */ From c7170973edddb21f16fd4a0fbf1db04d4a631d15 Mon Sep 17 00:00:00 2001 From: Johan Cwiklinski Date: Wed, 23 Sep 2026 23:03:38 +0200 Subject: [PATCH 6/6] Improve search forms Fix bottom spacing of search forms Choose the category from the search form, on its own line with activity and buttons Put activity, stock and buttons on their own line in status and category search forms Keep status search radio groups side by side, buttons below --- .../Controllers/Crud/ObjectsController.php | 9 ++- templates/default/categories_list.html.twig | 24 +++---- templates/default/objects_list.html.twig | 71 ++++++++----------- templates/default/status_list.html.twig | 28 ++++---- .../tests/units/ObjectsController.php | 15 ++++ 5 files changed, 80 insertions(+), 67 deletions(-) diff --git a/lib/GaletteObjectsLend/Controllers/Crud/ObjectsController.php b/lib/GaletteObjectsLend/Controllers/Crud/ObjectsController.php index a45d6d16..54be7a61 100644 --- a/lib/GaletteObjectsLend/Controllers/Crud/ObjectsController.php +++ b/lib/GaletteObjectsLend/Controllers/Crud/ObjectsController.php @@ -139,7 +139,10 @@ public function list(Request $request, Response $response, ?string $option = nul $cat_filters = new CategoriesList(); $cat_filters->active_filter = Categories::ACTIVE_CATEGORIES; //retrieve only active categories $cat_filters->not_empty = true; //retrieve only categories with objects - $cat_filters->setObjectsFilter($filters); //search for categories corresponding to filtered objects + //search for categories corresponding to filtered objects, whatever the chosen category + $objects_filters = clone $filters; + $objects_filters->category_filter = null; + $cat_filters->setObjectsFilter($objects_filters); $categories = new Categories($this->zdb, $this->preferences, $this->login, $cat_filters); $categories_list = $categories->getCategoriesList(true, false, false); @@ -186,6 +189,10 @@ public function filter(Request $request, Response $response): Response if (isset($post['field_filter'])) { $filters->field_filter = $post['field_filter']; } + //category, empty for all + if (isset($post['category_filter'])) { + $filters->category_filter = $post['category_filter'] === '' ? null : $post['category_filter']; + } //activity to filter if (isset($post['active_filter'])) { if (is_numeric($post['active_filter'])) { diff --git a/templates/default/categories_list.html.twig b/templates/default/categories_list.html.twig index 7c944978..8112c029 100644 --- a/templates/default/categories_list.html.twig +++ b/templates/default/categories_list.html.twig @@ -50,37 +50,37 @@ {% block search %}
-
-
- - -
+
+ + +
+
-
+
- +
- +
- +
-
-
- - {% if lendsprefs.VIEW_CATEGORY and categories|length > 0 %} -
- {% endif %} {% endblock %} {% block body %} diff --git a/templates/default/status_list.html.twig b/templates/default/status_list.html.twig index df1a7d30..6d43ba6d 100644 --- a/templates/default/status_list.html.twig +++ b/templates/default/status_list.html.twig @@ -63,60 +63,62 @@ {% block search %}
-
+
- +
- +
- +
- +
- +
- +
- +
- +
- +
-
-
+
+
+