diff --git a/lib/GaletteObjectsLend/Controllers/Crud/AbstractListController.php b/lib/GaletteObjectsLend/Controllers/Crud/AbstractListController.php new file mode 100644 index 0000000..f83edca --- /dev/null +++ b/lib/GaletteObjectsLend/Controllers/Crud/AbstractListController.php @@ -0,0 +1,402 @@ + + * + * @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); + + try { + $entity->store(); + $errors = $this->afterStore($entity, $request, $post); + } catch (Throwable $e) { + $this->logError('store', $entity, $e); + $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 + { + $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/CategoriesController.php b/lib/GaletteObjectsLend/Controllers/Crud/CategoriesController.php index a9ab548..fdb274d 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->login, $filters); + $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 2d0dab1..54be7a6 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 @@ -45,6 +46,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 +107,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) { @@ -106,10 +127,10 @@ 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; + $this->session->{$this->getFiltersKey()} = $filters; //assign pagination variables to the template and add pagination links $filters->setViewCommonsFilters($lendsprefs, $this->view); @@ -118,9 +139,12 @@ 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 - $categories = new Categories($this->zdb, $this->login, $cat_filters); - $categories_list = $categories->getCategoriesList(true, null, false); + //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); // display page $this->view->render( @@ -151,11 +175,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'])) { @@ -169,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'])) { @@ -181,7 +205,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 +249,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 @@ -280,7 +300,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 +311,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); @@ -351,7 +371,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']); @@ -369,7 +400,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 ); } @@ -513,7 +544,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( @@ -560,8 +602,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 +830,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 @@ -834,7 +876,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 +897,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,13 +914,9 @@ 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, $lendsprefs, $filters); + $objects = new Objects($this->zdb, $this->preferences, $this->login, $lendsprefs, $filters); if (!is_array($post['id'])) { $ids = (array)$post['id']; @@ -886,11 +924,17 @@ protected function doDelete(array $args, array $post): bool $ids = $post['id']; } - $result = $objects->removeObjects($ids); - if ($result) { - unset($this->session->objectslend_filter_objects); + 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/Controllers/Crud/StatusController.php b/lib/GaletteObjectsLend/Controllers/Crud/StatusController.php index c71e3e0..4514037 100644 --- a/lib/GaletteObjectsLend/Controllers/Crud/StatusController.php +++ b/lib/GaletteObjectsLend/Controllers/Crud/StatusController.php @@ -10,84 +10,76 @@ 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']; } + } - $statuses = new Status($this->zdb, $this->login, $filters); + /** + * 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); if (count($statuses->getActiveStockStatuses()) == 0) { @@ -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 773208b..b5b8b7a 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; @@ -37,7 +38,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, @@ -58,17 +59,14 @@ 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; } - $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/Entity/LendCategory.php b/lib/GaletteObjectsLend/Entity/LendCategory.php index 0aa2339..ffa2661 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 919707e..dc60ad7 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 7786f4e..3f0dd6e 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 1d8406e..2f87776 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/Filters/CategoriesList.php b/lib/GaletteObjectsLend/Filters/CategoriesList.php index 41af683..68fd044 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; @@ -23,27 +22,13 @@ * @property ?int $active_filter * @property ?bool $not_empty * @property ?ObjectsList $objects_filters - * @property string $query */ - class CategoriesList extends Pagination { - //filters - private ?string $filter_str; - private ?int $active_filter; - private ?bool $not_empty; - private ?ObjectsList $objects_filters; - - protected string $query; + use ListFilters; - /** @var array */ - protected array $categorylist_fields = [ - 'filter_str', - 'active_filter', - 'not_empty', - 'objects_filters', - 'query' - ]; + private ?bool $not_empty = null; + private ?ObjectsList $objects_filters = null; /** * Default constructor @@ -67,88 +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 'query': - 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 0000000..e56de44 --- /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 08f4c33..bbc0967 100644 --- a/lib/GaletteObjectsLend/Filters/ObjectsList.php +++ b/lib/GaletteObjectsLend/Filters/ObjectsList.php @@ -21,35 +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 $query + * @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; - - protected string $query; + use ListFilters; - /** @var array */ - protected array $objectslist_fields = [ - 'filter_str', - 'category_filter', - 'active_filter', - 'field_filter', - 'selected', - 'query' - ]; + private ?int $category_filter = null; + private ?int $field_filter = null; + /** @var array */ + private array $selected = []; /** * Default constructor @@ -73,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 = []; } @@ -116,129 +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': - case 'query': - $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 b369cf6..64ceaf3 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; @@ -22,25 +21,12 @@ * @property ?string $filter_str * @property ?int $active_filter * @property ?int $stock_filter - * @property string $query */ - class StatusList extends Pagination { - //filters - private ?string $filter_str; - private ?int $active_filter; - private ?int $stock_filter; - - protected string $query; + use ListFilters; - /** @var array */ - protected array $statuslist_fields = [ - 'filter_str', - 'active_filter', - 'stock_filter', - 'query' - ]; + private ?int $stock_filter = null; /** * Returns the field we want to default set order to @@ -56,103 +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': - case 'query': - $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/lib/GaletteObjectsLend/LendService.php b/lib/GaletteObjectsLend/LendService.php index fc87b71..8730d29 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); } /** @@ -282,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()]) @@ -429,7 +430,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 0000000..4b303aa --- /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 1f3336e..acf9f6d 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 683f4b3..45b8bd7 100644 --- a/lib/GaletteObjectsLend/Repository/Objects.php +++ b/lib/GaletteObjectsLend/Repository/Objects.php @@ -10,32 +10,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 +58,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); } /** @@ -131,7 +111,7 @@ public function getObjectsList( * * @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 { @@ -141,71 +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; - } - - /** - * 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 - ); } /** @@ -264,281 +199,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 - * - * @param array $fields Fields list to ensure ORDER clause - * references selected fields. Optional. + * Create an entity from a resultset row * - * @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 + * + * Objects table must be aliased as "o", and categories one as "c". * - * @param Select $select Original select + * @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 (count($this->filters->selected) > 0) { + $select->where->in('o.' . self::PK, $this->filters->selected); + } - if ($this->filters->filter_str != '') { - $token = $this->zdb->platform->quoteValue( - '%' . strtolower($this->filters->filter_str) . '%' - ); - - 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/Rents.php b/lib/GaletteObjectsLend/Repository/Rents.php index e0a3b38..ca19deb 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/lib/GaletteObjectsLend/Repository/Status.php b/lib/GaletteObjectsLend/Repository/Status.php index 06a3b75..82843c2 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 8d425e6..10f2856 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 @@ -65,105 +59,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/templates/default/categories_list.html.twig b/templates/default/categories_list.html.twig index 7c94497..8112c02 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 %} -
-
- - {{ _T("Categories", "objectslend") }} -
- -
- {% endif %} {% endblock %} {% block body %} diff --git a/templates/default/status_list.html.twig b/templates/default/status_list.html.twig index df1a7d3..6d43ba6 100644 --- a/templates/default/status_list.html.twig +++ b/templates/default/status_list.html.twig @@ -63,60 +63,62 @@ {% block search %}
-
+
- +
- +
- +
- +
- +
- +
- +
- +
- +
-
-
+
+
+