diff --git a/autoload/Domain/Category/CategoryRepository.php b/autoload/Domain/Category/CategoryRepository.php index d514b52..50990e6 100644 --- a/autoload/Domain/Category/CategoryRepository.php +++ b/autoload/Domain/Category/CategoryRepository.php @@ -15,6 +15,26 @@ class CategoryRepository 6 => 'alfabetycznie - Z - A', ]; + private const SORT_ORDER_SQL = [ + 0 => 'q1.date_add ASC', + 1 => 'q1.date_add DESC', + 2 => 'q1.date_modify ASC', + 3 => 'q1.date_modify DESC', + 4 => 'q1.o ASC', + 5 => 'q1.name ASC', + 6 => 'q1.name DESC', + ]; + + private const PRODUCTS_PER_PAGE = 12; + + private const LANGUAGE_FALLBACK_NAME_SQL = '(CASE ' + . 'WHEN copy_from IS NULL THEN name ' + . 'WHEN copy_from IS NOT NULL THEN (' + . 'SELECT name FROM pp_shop_products_langs ' + . 'WHERE lang_id = pspl.copy_from AND product_id = psp.id' + . ') ' + . 'END) AS name'; + public function __construct($db) { $this->db = $db; @@ -317,6 +337,330 @@ class CategoryRepository return (string)$title[0]; } + // ===== Frontend methods ===== + + public function getCategorySort(int $categoryId): int + { + if ($categoryId <= 0) { + return 0; + } + + $cacheHandler = new \Shared\Cache\CacheHandler(); + $cacheKey = "get_category_sort:$categoryId"; + $objectData = $cacheHandler->get($cacheKey); + + if ($objectData) { + return (int)unserialize($objectData); + } + + $sortType = (int)$this->db->get('pp_shop_categories', 'sort_type', ['id' => $categoryId]); + $cacheHandler->set($cacheKey, $sortType); + + return $sortType; + } + + public function categoryName(int $categoryId, string $langId): string + { + if ($categoryId <= 0 || $langId === '') { + return ''; + } + + $cacheHandler = new \Shared\Cache\CacheHandler(); + $cacheKey = "category_name:{$langId}:{$categoryId}"; + $objectData = $cacheHandler->get($cacheKey); + + if ($objectData) { + return (string)unserialize($objectData); + } + + $name = $this->db->get('pp_shop_categories_langs', 'title', [ + 'AND' => [ + 'category_id' => $categoryId, + 'lang_id' => $langId, + ], + ]); + + $cacheHandler->set($cacheKey, $name); + + return (string)$name; + } + + public function categoryUrl(int $categoryId, string $langId): string + { + if ($categoryId <= 0) { + return '#'; + } + + $category = $this->frontCategoryDetails($categoryId, $langId); + if (empty($category)) { + return '#'; + } + + $url = !empty($category['language']['seo_link']) + ? '/' . $category['language']['seo_link'] + : '/k-' . $category['id'] . '-' . \Shared\Helpers\Helpers::seo($category['language']['title'] ?? ''); + + $currentLang = \Shared\Helpers\Helpers::get_session('current-lang'); + $defaultLang = (new \Domain\Languages\LanguagesRepository($this->db))->defaultLanguage(); + + if ($currentLang != $defaultLang && $url !== '#') { + $url = '/' . $currentLang . $url; + } + + return $url; + } + + /** + * @return array + */ + public function frontCategoryDetails(int $categoryId, string $langId): array + { + if ($categoryId <= 0) { + return []; + } + + $cacheHandler = new \Shared\Cache\CacheHandler(); + $cacheKey = "front_category_details:{$categoryId}:{$langId}"; + $objectData = $cacheHandler->get($cacheKey); + + if ($objectData) { + return unserialize($objectData); + } + + $category = $this->db->get('pp_shop_categories', '*', ['id' => $categoryId]); + if (!is_array($category)) { + return []; + } + + $category['language'] = $this->db->get('pp_shop_categories_langs', '*', [ + 'AND' => [ + 'category_id' => $categoryId, + 'lang_id' => $langId, + ], + ]); + + $cacheHandler->set($cacheKey, $category); + + return $category; + } + + /** + * @return array> + */ + public function categoriesTree(string $langId, ?int $parentId = null): array + { + $cacheHandler = new \Shared\Cache\CacheHandler(); + $cacheKey = "categories_tree:{$langId}:{$parentId}"; + $objectData = $cacheHandler->get($cacheKey); + + if ($objectData) { + return unserialize($objectData); + } + + $categories = []; + $results = $this->db->select('pp_shop_categories', 'id', [ + 'parent_id' => $parentId, + 'ORDER' => ['o' => 'ASC'], + ]); + + if (is_array($results)) { + foreach ($results as $row) { + $category = $this->frontCategoryDetails((int)$row, $langId); + $category['categories'] = $this->categoriesTree($langId, (int)$row); + $categories[] = $category; + } + } + + $cacheHandler->set($cacheKey, $categories); + + return $categories; + } + + /** + * @return array + */ + public function blogCategoryProducts(int $categoryId, string $langId, int $limit): array + { + if ($categoryId <= 0 || $langId === '' || $limit <= 0) { + return []; + } + + $cacheHandler = new \Shared\Cache\CacheHandler(); + $cacheKey = "blog_category_products:{$categoryId}:{$langId}:{$limit}"; + $objectData = $cacheHandler->get($cacheKey); + + if ($objectData) { + return unserialize($objectData); + } + + $rows = $this->db->query( + 'SELECT * FROM (' + . 'SELECT ' + . 'psp.id, date_modify, date_add, o, ' + . self::LANGUAGE_FALLBACK_NAME_SQL . ' ' + . 'FROM ' + . 'pp_shop_products_categories AS pspc ' + . 'INNER JOIN pp_shop_products AS psp ON psp.id = pspc.product_id ' + . 'INNER JOIN pp_shop_products_langs AS pspl ON pspl.product_id = pspc.product_id ' + . 'WHERE ' + . 'status = 1 AND category_id = :category_id AND lang_id = :lang_id' + . ') AS q1 ' + . 'WHERE ' + . 'q1.name IS NOT NULL ' + . 'ORDER BY ' + . 'RAND() ' + . 'LIMIT ' . (int)$limit, + [ + ':category_id' => $categoryId, + ':lang_id' => $langId, + ] + ); + + $output = []; + if ($rows) { + foreach ($rows->fetchAll() as $row) { + $output[] = $row['id']; + } + } + + $cacheHandler->set($cacheKey, $output); + + return $output; + } + + public function categoryProductsCount(int $categoryId, string $langId): int + { + if ($categoryId <= 0 || $langId === '') { + return 0; + } + + $cacheHandler = new \Shared\Cache\CacheHandler(); + $cacheKey = "category_products_count:{$categoryId}:{$langId}"; + $objectData = $cacheHandler->get($cacheKey); + + if ($objectData) { + return (int)unserialize($objectData); + } + + $rows = $this->db->query( + 'SELECT COUNT(0) FROM (' + . 'SELECT ' + . 'psp.id, ' + . self::LANGUAGE_FALLBACK_NAME_SQL . ' ' + . 'FROM ' + . 'pp_shop_products_categories AS pspc ' + . 'INNER JOIN pp_shop_products AS psp ON psp.id = pspc.product_id ' + . 'INNER JOIN pp_shop_products_langs AS pspl ON pspl.product_id = pspc.product_id ' + . 'WHERE ' + . 'status = 1 AND category_id = :category_id AND lang_id = :lang_id' + . ') AS q1 ' + . 'WHERE ' + . 'q1.name IS NOT NULL', + [ + ':category_id' => $categoryId, + ':lang_id' => $langId, + ] + ); + + $productsCount = 0; + if ($rows) { + $result = $rows->fetchAll(); + $productsCount = (int)($result[0][0] ?? 0); + } + + $cacheHandler->set($cacheKey, $productsCount); + + return $productsCount; + } + + /** + * @return array + */ + public function productsId(int $categoryId, int $sortType, string $langId, int $productsLimit, int $from): array + { + if ($categoryId <= 0 || $langId === '') { + return []; + } + + $order = self::SORT_ORDER_SQL[$sortType] ?? self::SORT_ORDER_SQL[0]; + $today = date('Y-m-d'); + + $cacheHandler = new \Shared\Cache\CacheHandler(); + $cacheKey = "products_id:{$categoryId}:{$sortType}:{$langId}:{$productsLimit}:{$from}"; + $objectData = $cacheHandler->get($cacheKey); + + if ($objectData) { + return unserialize($objectData); + } + + $rows = $this->db->query( + 'SELECT * FROM (' + . 'SELECT ' + . 'psp.id, date_modify, date_add, o, ' + . self::LANGUAGE_FALLBACK_NAME_SQL . ', ' + . '(CASE ' + . 'WHEN new_to_date >= :today THEN new_to_date ' + . 'WHEN new_to_date < :today2 THEN null ' + . 'END) AS new_to_date, ' + . '(CASE WHEN (quantity + (SELECT IFNULL(SUM(quantity),0) FROM pp_shop_products WHERE parent_id = psp.id)) > 0 THEN 1 ELSE 0 END) AS total_quantity ' + . 'FROM ' + . 'pp_shop_products_categories AS pspc ' + . 'INNER JOIN pp_shop_products AS psp ON psp.id = pspc.product_id ' + . 'INNER JOIN pp_shop_products_langs AS pspl ON pspl.product_id = pspc.product_id ' + . 'WHERE ' + . 'status = 1 AND category_id = :category_id AND lang_id = :lang_id' + . ') AS q1 ' + . 'WHERE ' + . 'q1.name IS NOT NULL ' + . 'ORDER BY ' + . $order . ' ' + . 'LIMIT ' . (int)$from . ',' . (int)$productsLimit, + [ + ':category_id' => $categoryId, + ':lang_id' => $langId, + ':today' => $today, + ':today2' => $today, + ] + ); + + $output = []; + if ($rows) { + foreach ($rows->fetchAll() as $row) { + $output[] = $row['id']; + } + } + + $cacheHandler->set($cacheKey, $output); + + return $output; + } + + /** + * @return array{products: array, ls: int} + */ + public function paginatedCategoryProducts(int $categoryId, int $sortType, string $langId, int $page): array + { + $count = $this->categoryProductsCount($categoryId, $langId); + if ($count <= 0) { + return ['products' => [], 'ls' => 0]; + } + + $totalPages = (int)ceil($count / self::PRODUCTS_PER_PAGE); + + if ($page < 1) { + $page = 1; + } elseif ($page > $totalPages) { + $page = $totalPages; + } + + $from = self::PRODUCTS_PER_PAGE * ($page - 1); + + return [ + 'products' => $this->productsId($categoryId, $sortType, $langId, self::PRODUCTS_PER_PAGE, $from), + 'ls' => $totalPages, + ]; + } + private function maxOrder(): int { return (int)$this->db->max('pp_shop_categories', 'o'); diff --git a/autoload/Domain/Client/ClientRepository.php b/autoload/Domain/Client/ClientRepository.php index eefb72c..4dfec8f 100644 --- a/autoload/Domain/Client/ClientRepository.php +++ b/autoload/Domain/Client/ClientRepository.php @@ -234,6 +234,287 @@ class ClientRepository ]; } + // ===== Frontend methods ===== + + /** + * @return array|null + */ + public function clientDetails(int $clientId): ?array + { + if ($clientId <= 0) { + return null; + } + + return $this->db->get('pp_shop_clients', '*', ['id' => $clientId]) ?: null; + } + + public function clientEmail(int $clientId): ?string + { + if ($clientId <= 0) { + return null; + } + + $email = $this->db->get('pp_shop_clients', 'email', ['id' => $clientId]); + + return $email ? (string)$email : null; + } + + /** + * @return array> + */ + public function clientAddresses(int $clientId): array + { + if ($clientId <= 0) { + return []; + } + + $rows = $this->db->select('pp_shop_clients_addresses', '*', ['client_id' => $clientId]); + + return is_array($rows) ? $rows : []; + } + + /** + * @return array|null + */ + public function addressDetails(int $addressId): ?array + { + if ($addressId <= 0) { + return null; + } + + return $this->db->get('pp_shop_clients_addresses', '*', ['id' => $addressId]) ?: null; + } + + public function addressDelete(int $addressId): bool + { + if ($addressId <= 0) { + return false; + } + + return (bool)$this->db->delete('pp_shop_clients_addresses', ['id' => $addressId]); + } + + /** + * @param array $data Keys: name, surname, street, postal_code, city, phone + */ + public function addressSave(int $clientId, ?int $addressId, array $data): bool + { + if ($clientId <= 0) { + return false; + } + + $row = [ + 'name' => (string)($data['name'] ?? ''), + 'surname' => (string)($data['surname'] ?? ''), + 'street' => (string)($data['street'] ?? ''), + 'postal_code' => (string)($data['postal_code'] ?? ''), + 'city' => (string)($data['city'] ?? ''), + 'phone' => (string)($data['phone'] ?? ''), + ]; + + if (!$addressId || $addressId <= 0) { + $row['client_id'] = $clientId; + return (bool)$this->db->insert('pp_shop_clients_addresses', $row); + } + + return (bool)$this->db->update('pp_shop_clients_addresses', $row, [ + 'AND' => [ + 'client_id' => $clientId, + 'id' => $addressId, + ], + ]); + } + + public function markAddressAsCurrent(int $clientId, int $addressId): bool + { + if ($clientId <= 0 || $addressId <= 0) { + return false; + } + + $this->db->update('pp_shop_clients_addresses', ['current' => 0], ['client_id' => $clientId]); + $this->db->update('pp_shop_clients_addresses', ['current' => 1], [ + 'AND' => [ + 'client_id' => $clientId, + 'id' => $addressId, + ], + ]); + + return true; + } + + /** + * @return array> + */ + public function clientOrders(int $clientId): array + { + if ($clientId <= 0) { + return []; + } + + $rows = $this->db->select('pp_shop_orders', 'id', [ + 'client_id' => $clientId, + 'ORDER' => ['date_order' => 'DESC'], + ]); + + $orders = []; + if (is_array($rows)) { + foreach ($rows as $row) { + $orders[] = \front\factory\ShopOrder::order_details($row); + } + } + + return $orders; + } + + /** + * @return array{status: string, client?: array, hash?: string, code?: string} + */ + public function authenticate(string $email, string $password): array + { + $email = trim($email); + $password = trim($password); + + if ($email === '' || $password === '') { + return ['status' => 'error', 'code' => 'logowanie-nieudane']; + } + + $client = $this->db->get('pp_shop_clients', [ + 'id', 'password', 'register_date', 'hash', 'status', + ], ['email' => $email]); + + if (!$client) { + return ['status' => 'error', 'code' => 'logowanie-nieudane']; + } + + if (!(int)$client['status']) { + return ['status' => 'inactive', 'hash' => $client['hash']]; + } + + if ($client['password'] !== md5($client['register_date'] . $password)) { + return ['status' => 'error', 'code' => 'logowanie-blad-nieprawidlowe-haslo']; + } + + $fullClient = $this->clientDetails((int)$client['id']); + + return ['status' => 'ok', 'client' => $fullClient]; + } + + /** + * @return array{id: int, hash: string}|null Null when email already taken + */ + public function createClient(string $email, string $password, bool $agreementMarketing): ?array + { + $email = trim($email); + if ($email === '' || $password === '') { + return null; + } + + if ($this->db->count('pp_shop_clients', ['email' => $email])) { + return null; + } + + $hash = md5(time() . $email); + $registerDate = date('Y-m-d H:i:s'); + + $inserted = $this->db->insert('pp_shop_clients', [ + 'email' => $email, + 'password' => md5($registerDate . $password), + 'hash' => $hash, + 'agremment_marketing' => $agreementMarketing ? 1 : 0, + 'register_date' => $registerDate, + ]); + + if (!$inserted) { + return null; + } + + return [ + 'id' => (int)$this->db->id(), + 'hash' => $hash, + ]; + } + + /** + * Confirms registration. Returns client email on success, null on failure. + */ + public function confirmRegistration(string $hash): ?string + { + $hash = trim($hash); + if ($hash === '') { + return null; + } + + $id = $this->db->get('pp_shop_clients', 'id', [ + 'AND' => ['hash' => $hash, 'status' => 0], + ]); + + if (!$id) { + return null; + } + + $this->db->update('pp_shop_clients', ['status' => 1], ['id' => $id]); + + $email = $this->db->get('pp_shop_clients', 'email', ['id' => $id]); + + return $email ? (string)$email : null; + } + + /** + * Generates new password. Returns [email, password] on success, null on failure. + * + * @return array{email: string, password: string}|null + */ + public function generateNewPassword(string $hash): ?array + { + $hash = trim($hash); + if ($hash === '') { + return null; + } + + $data = $this->db->get('pp_shop_clients', ['id', 'email', 'register_date'], [ + 'AND' => ['hash' => $hash, 'status' => 1, 'password_recovery' => 1], + ]); + + if (!$data) { + return null; + } + + $newPassword = substr(md5(time()), 0, 10); + + $this->db->update('pp_shop_clients', [ + 'password_recovery' => 0, + 'password' => md5($data['register_date'] . $newPassword), + ], ['id' => $data['id']]); + + return [ + 'email' => (string)$data['email'], + 'password' => $newPassword, + ]; + } + + /** + * Initiates password recovery. Returns hash on success, null on failure. + */ + public function initiatePasswordRecovery(string $email): ?string + { + $email = trim($email); + if ($email === '') { + return null; + } + + $hash = $this->db->get('pp_shop_clients', 'hash', [ + 'AND' => ['email' => $email, 'status' => 1], + ]); + + if (!$hash) { + return null; + } + + $this->db->update('pp_shop_clients', ['password_recovery' => 1], ['email' => $email]); + + return (string)$hash; + } + private function normalizeTextFilter($value): string { $value = trim((string)$value); diff --git a/autoload/front/Controllers/ShopBasketController.php b/autoload/front/Controllers/ShopBasketController.php index eeea899..db10145 100644 --- a/autoload/front/Controllers/ShopBasketController.php +++ b/autoload/front/Controllers/ShopBasketController.php @@ -266,7 +266,7 @@ class ShopBasketController 'basket' => \Shared\Helpers\Helpers::get_session( 'basket' ), 'transport' => \front\factory\ShopTransport::transport( \Shared\Helpers\Helpers::get_session( 'basket-transport-method-id' ) ), 'payment_method' => \front\factory\ShopPaymentMethod::payment_method( \Shared\Helpers\Helpers::get_session( 'basket-payment-method-id' ) ), - 'addresses' => \front\factory\ShopClient::client_addresses( $client[ 'id' ] ), + 'addresses' => ( new \Domain\Client\ClientRepository( $GLOBALS['mdb'] ) )->clientAddresses( (int)$client['id'] ), 'settings' => $settings, 'coupon' => \Shared\Helpers\Helpers::get_session( 'coupon' ), 'basket_message' => \Shared\Helpers\Helpers::get_session( 'basket_message' ) diff --git a/autoload/front/Controllers/ShopClientController.php b/autoload/front/Controllers/ShopClientController.php new file mode 100644 index 0000000..fa58f07 --- /dev/null +++ b/autoload/front/Controllers/ShopClientController.php @@ -0,0 +1,354 @@ +clientRepo = $clientRepo; + } + + public function markAddressAsCurrent() + { + $client = \Shared\Helpers\Helpers::get_session('client'); + if (!$client) { + return false; + } + + $this->clientRepo->markAddressAsCurrent( + (int)$client['id'], + (int)\Shared\Helpers\Helpers::get('address_id') + ); + exit; + } + + public function addressDelete() + { + $client = \Shared\Helpers\Helpers::get_session('client'); + if (!$client) { + header('Location: /logowanie'); + exit; + } + + $address = $this->clientRepo->addressDetails((int)\Shared\Helpers\Helpers::get('id')); + if (!$address || $address['client_id'] != $client['id']) { + header('Location: /panel-klienta/adresy'); + exit; + } + + if ($this->clientRepo->addressDelete((int)\Shared\Helpers\Helpers::get('id'))) { + \Shared\Helpers\Helpers::alert(\Shared\Helpers\Helpers::lang('adres-usuniety-komunikat')); + } else { + \Shared\Helpers\Helpers::error(\Shared\Helpers\Helpers::lang('adres-usuniety-blad')); + } + + header('Location: /panel-klienta/adresy'); + exit; + } + + public function addressEdit() + { + global $page, $settings; + + $page['language']['meta_title'] = \Shared\Helpers\Helpers::lang('meta-title-edycja-adresu') . ' | ' . $settings['firm_name']; + + $client = \Shared\Helpers\Helpers::get_session('client'); + if (!$client) { + header('Location: /logowanie'); + exit; + } + + $addressId = (int)\Shared\Helpers\Helpers::get('id'); + $address = $this->clientRepo->addressDetails($addressId); + if ($address && $address['client_id'] != $client['id']) { + $address = null; + } + + return \front\Views\ShopClient::addressEdit([ + 'address' => $address, + ]); + } + + public function addressSave() + { + $client = \Shared\Helpers\Helpers::get_session('client'); + if (!$client) { + header('Location: /logowanie'); + exit; + } + + $addressId = (int)\Shared\Helpers\Helpers::get('address_id'); + $data = [ + 'name' => \Shared\Helpers\Helpers::get('name', true), + 'surname' => \Shared\Helpers\Helpers::get('surname', true), + 'street' => \Shared\Helpers\Helpers::get('street'), + 'postal_code' => \Shared\Helpers\Helpers::get('postal_code', true), + 'city' => \Shared\Helpers\Helpers::get('city', true), + 'phone' => \Shared\Helpers\Helpers::get('phone', true), + ]; + + if ($this->clientRepo->addressSave((int)$client['id'], $addressId ?: null, $data)) { + $msg = $addressId + ? \Shared\Helpers\Helpers::lang('zmiana-adresu-sukces') + : \Shared\Helpers\Helpers::lang('dodawanie-nowego-adresu-sukces'); + \Shared\Helpers\Helpers::alert($msg); + } else { + $msg = $addressId + ? \Shared\Helpers\Helpers::lang('zmiana-adresu-blad') + : \Shared\Helpers\Helpers::lang('dodawanie-nowego-adresu-blad'); + \Shared\Helpers\Helpers::error($msg); + } + + header('Location: /panel-klienta/adresy'); + exit; + } + + public function clientAddresses() + { + global $page, $settings; + + $page['language']['meta_title'] = \Shared\Helpers\Helpers::lang('meta-title-lista-adresow') . ' | ' . $settings['firm_name']; + + $client = \Shared\Helpers\Helpers::get_session('client'); + if (!$client) { + header('Location: /logowanie'); + exit; + } + + return \front\Views\ShopClient::clientAddresses([ + 'client' => $client, + 'addresses' => $this->clientRepo->clientAddresses((int)$client['id']), + ]); + } + + public function clientOrders() + { + global $page, $settings; + + $page['language']['meta_title'] = \Shared\Helpers\Helpers::lang('meta-title-historia-zamowien') . ' | ' . $settings['firm_name']; + + $client = \Shared\Helpers\Helpers::get_session('client'); + if (!$client) { + header('Location: /logowanie'); + exit; + } + + return \front\Views\ShopClient::clientOrders([ + 'client' => $client, + 'orders' => $this->clientRepo->clientOrders((int)$client['id']), + 'statuses' => \shop\Order::order_statuses(), + ]); + } + + public function newPassword() + { + $result = $this->clientRepo->generateNewPassword( + (string)\Shared\Helpers\Helpers::get('hash') + ); + + if ($result) { + $text = $this->buildEmailBody('#nowe-haslo', [ + '[HASLO]' => $result['password'], + ]); + \Shared\Helpers\Helpers::send_email( + $result['email'], + \Shared\Helpers\Helpers::lang('nowe-haslo-w-sklepie'), + $text + ); + \Shared\Helpers\Helpers::alert(\Shared\Helpers\Helpers::lang('nowe-haslo-zostalo-wyslane-na-twoj-adres-email')); + } + + header('Location: /logowanie'); + exit; + } + + public function sendEmailPasswordRecovery() + { + $hash = $this->clientRepo->initiatePasswordRecovery( + (string)\Shared\Helpers\Helpers::get('email') + ); + + if ($hash) { + $text = $this->buildEmailBody('#odzyskiwanie-hasla-link', [ + '[LINK]' => '/shopClient/new_password/hash=' . $hash, + ]); + \Shared\Helpers\Helpers::send_email( + (string)\Shared\Helpers\Helpers::get('email'), + \Shared\Helpers\Helpers::lang('generowanie-nowego-hasla-w-sklepie'), + $text + ); + \Shared\Helpers\Helpers::alert(\Shared\Helpers\Helpers::lang('odzyskiwanie-hasla-link-komunikat')); + } else { + \Shared\Helpers\Helpers::alert(\Shared\Helpers\Helpers::lang('odzyskiwanie-hasla-blad')); + } + + header('Location: /logowanie'); + exit; + } + + public function recoverPassword() + { + global $page, $settings; + + $page['language']['meta_title'] = \Shared\Helpers\Helpers::lang('meta-title-odzyskiwanie-hasla') . ' | ' . $settings['firm_name']; + + return \front\Views\ShopClient::recoverPassword(); + } + + public function logout() + { + \Shared\Helpers\Helpers::delete_session('client'); + header('Location: /'); + exit; + } + + public function login() + { + $result = $this->clientRepo->authenticate( + (string)\Shared\Helpers\Helpers::get('email'), + (string)\Shared\Helpers\Helpers::get('password') + ); + + if ($result['status'] === 'inactive') { + $link = '' + . ucfirst(\Shared\Helpers\Helpers::lang('wyslij-link-ponownie')) . ''; + \Shared\Helpers\Helpers::alert( + str_replace('[LINK]', $link, \Shared\Helpers\Helpers::lang('logowanie-blad-nieaktywne-konto')) + ); + header('Location: /logowanie'); + exit; + } + + if ($result['status'] !== 'ok') { + \Shared\Helpers\Helpers::alert(\Shared\Helpers\Helpers::lang($result['code'])); + header('Location: /logowanie'); + exit; + } + + \Shared\Helpers\Helpers::set_session('client', $result['client']); + \Shared\Helpers\Helpers::alert(\Shared\Helpers\Helpers::lang('logowanie-udane')); + + $redirect = \Shared\Helpers\Helpers::get('redirect'); + header('Location: ' . ($redirect ? $redirect : '/panel-klienta')); + exit; + } + + public function confirm() + { + $email = $this->clientRepo->confirmRegistration( + (string)\Shared\Helpers\Helpers::get('hash') + ); + + if ($email) { + $text = $this->buildEmailBody('#potwierdzenie-aktywacji-konta'); + \Shared\Helpers\Helpers::send_email( + $email, + \Shared\Helpers\Helpers::lang('potwierdzenie-aktywacji-konta-w-sklepie') . ' ' . \Shared\Helpers\Helpers::lang('#nazwa-serwisu'), + $text + ); + \Shared\Helpers\Helpers::alert(\Shared\Helpers\Helpers::lang('rejestracja-potwierdzenie')); + } + + header('Location: /logowanie'); + exit; + } + + public function signup() + { + $email = (string)\Shared\Helpers\Helpers::get('email'); + $password = (string)\Shared\Helpers\Helpers::get('password'); + + $created = $this->clientRepo->createClient( + $email, + $password, + (bool)\Shared\Helpers\Helpers::get('agremment_marketing') + ); + + if (!$created) { + echo json_encode([ + 'status' => 'bad', + 'msg' => \Shared\Helpers\Helpers::lang('rejestracja-email-zajety'), + ]); + exit; + } + + $text = $this->buildEmailBody('#potwierdzenie-rejestracji', [ + '[LINK]' => '/shopClient/confirm/hash=' . $created['hash'], + ]); + \Shared\Helpers\Helpers::send_email( + $email, + \Shared\Helpers\Helpers::lang('potwierdzenie-rejestracji-konta-w-sklepie') . ' ' . \Shared\Helpers\Helpers::lang('#nazwa-serwisu'), + $text + ); + + echo json_encode([ + 'status' => 'ok', + 'msg' => \Shared\Helpers\Helpers::lang('rejestracja-udana'), + ]); + exit; + } + + public function loginForm() + { + global $page, $settings; + + $page['language']['meta_title'] = \Shared\Helpers\Helpers::lang('meta-title-logowanie') . ' | ' . $settings['firm_name']; + $page['class'] = 'page-login-form'; + + $client = \Shared\Helpers\Helpers::get_session('client'); + if ($client) { + header('Location: /panel-klienta/zamowienia'); + exit; + } + + return \front\Views\ShopClient::loginForm(); + } + + public function registerForm() + { + global $page, $settings; + + $page['language']['meta_title'] = \Shared\Helpers\Helpers::lang('meta-title-rejestracja') . ' | ' . $settings['firm_name']; + + $client = \Shared\Helpers\Helpers::get_session('client'); + if ($client) { + header('Location: /panel-klienta/zamowienia'); + exit; + } + + return \front\Views\ShopClient::registerForm(); + } + + /** + * Builds email body from newsletter template with URL absolutization. + * + * @param array $replacements Placeholders to replace in the template + */ + private function buildEmailBody(string $templateName, array $replacements = []): string + { + $settings = $GLOBALS['settings']; + + $text = $settings['newsletter_header']; + $text .= (new \Domain\Newsletter\NewsletterRepository($GLOBALS['mdb']))->templateByName($templateName); + $text .= $settings['newsletter_footer']; + + $base = !empty($settings['ssl']) ? 'https' : 'http'; + $serverName = $_SERVER['SERVER_NAME'] ?? ''; + + $regex = "-(]+src\s*=\s*['\"])(((?!'|\"|https?://).)*)(['\"][^>]*>)-i"; + $text = preg_replace($regex, '$1' . $base . '://' . $serverName . '$2$4', $text); + + $regex = "-(]+href\s*=\s*['\"])(((?!'|\"|https?://).)*)(['\"][^>]*>)-i"; + $text = preg_replace($regex, '$1' . $base . '://' . $serverName . '$2$4', $text); + + foreach ($replacements as $placeholder => $value) { + $text = str_replace($placeholder, $value, $text); + } + + return $text; + } +} diff --git a/autoload/front/Views/ShopCategory.php b/autoload/front/Views/ShopCategory.php new file mode 100644 index 0000000..a3e9de9 --- /dev/null +++ b/autoload/front/Views/ShopCategory.php @@ -0,0 +1,66 @@ + $category, + ]); + } + + public static function categoryView($category, string $langId, int $currentPage = 1): string + { + global $settings, $page; + + $categoryRepo = new \Domain\Category\CategoryRepository($GLOBALS['mdb']); + + if (!$settings['infinitescroll']) { + $results = $categoryRepo->paginatedCategoryProducts( + (int)$category['id'], + (int)($category['sort_type'] ?? 0), + $langId, + $currentPage + ); + + $pager = ''; + if ($results['ls'] > 1) { + $tpl = new \Shared\Tpl\Tpl; + $tpl->ls = $results['ls']; + $tpl->bs = $currentPage > 0 ? $currentPage : 1; + $tpl->page = $page; + $tpl->link = !empty($category['language']['seo_link']) + ? $category['language']['seo_link'] + : 'k-' . $category['id'] . '-' . \Shared\Helpers\Helpers::seo($category['language']['title'] ?? ''); + $pager = $tpl->render('site/pager'); + } + + return \Shared\Tpl\Tpl::view('shop-category/category', [ + 'category' => $category, + 'products' => $results['products'], + 'pager' => $pager, + 'category_description' => $currentPage <= 1, + 'category_additional_text' => true, + ]); + } + + $productsCount = $categoryRepo->categoryProductsCount((int)$category['id'], $langId); + + return \Shared\Tpl\Tpl::view('shop-category/category-infinitescroll', [ + 'category' => $category, + 'products_count' => $productsCount, + 'category_description' => $currentPage <= 1, + 'category_additional_text' => true, + ]); + } + + public static function categories($categories, $currentCategory = 0, $level = 0): string + { + $tpl = new \Shared\Tpl\Tpl; + $tpl->level = $level; + $tpl->current_category = $currentCategory; + $tpl->categories = $categories; + return $tpl->render('shop-category/categories'); + } +} diff --git a/autoload/front/Views/ShopClient.php b/autoload/front/Views/ShopClient.php new file mode 100644 index 0000000..602eaf5 --- /dev/null +++ b/autoload/front/Views/ShopClient.php @@ -0,0 +1,80 @@ + $val) { + $tpl->$key = $val; + } + } + return $tpl->render('shop-client/address-edit'); + } + + public static function clientAddresses($values): string + { + $tpl = new \Shared\Tpl\Tpl; + if (is_array($values)) { + foreach ($values as $key => $val) { + $tpl->$key = $val; + } + } + return $tpl->render('shop-client/client-addresses'); + } + + public static function clientMenu($values): string + { + $tpl = new \Shared\Tpl\Tpl; + if (is_array($values)) { + foreach ($values as $key => $val) { + $tpl->$key = $val; + } + } + return $tpl->render('shop-client/client-menu'); + } + + public static function clientOrders($values): string + { + $tpl = new \Shared\Tpl\Tpl; + if (is_array($values)) { + foreach ($values as $key => $val) { + $tpl->$key = $val; + } + } + return $tpl->render('shop-client/client-orders'); + } + + public static function recoverPassword(): string + { + $tpl = new \Shared\Tpl\Tpl; + return $tpl->render('shop-client/recover-password'); + } + + public static function miniLogin(): string + { + global $client; + $tpl = new \Shared\Tpl\Tpl; + $tpl->client = $client; + return $tpl->render('shop-client/mini-login'); + } + + public static function loginForm($values = ''): string + { + $tpl = new \Shared\Tpl\Tpl; + if (is_array($values)) { + foreach ($values as $key => $val) { + $tpl->$key = $val; + } + } + return $tpl->render('shop-client/login-form'); + } + + public static function registerForm(): string + { + $tpl = new \Shared\Tpl\Tpl; + return $tpl->render('shop-client/register-form'); + } +} diff --git a/autoload/front/controls/class.ShopClient.php b/autoload/front/controls/class.ShopClient.php deleted file mode 100644 index 75cc000..0000000 --- a/autoload/front/controls/class.ShopClient.php +++ /dev/null @@ -1,212 +0,0 @@ - \front\factory\ShopClient::address_details( \Shared\Helpers\Helpers::get( 'id' ) ) - ] ); - } - - public static function address_save() - { - if ( !$client = \Shared\Helpers\Helpers::get_session( 'client' ) ) - { - header( 'Location: /logowanie' ); - exit; - } - - if ( \front\factory\ShopClient::address_save( $client['id'], \Shared\Helpers\Helpers::get( 'address_id' ), \Shared\Helpers\Helpers::get( 'name', true ), \Shared\Helpers\Helpers::get( 'surname', true ), \Shared\Helpers\Helpers::get( 'street' ), \Shared\Helpers\Helpers::get( 'postal_code', true ), \Shared\Helpers\Helpers::get( 'city', true ), \Shared\Helpers\Helpers::get( 'phone', true ) ) ) - { - \Shared\Helpers\Helpers::get( 'address_id' ) ? \Shared\Helpers\Helpers::alert( \Shared\Helpers\Helpers::lang( 'zmiana-adresu-sukces' ) ) : \Shared\Helpers\Helpers::alert( \Shared\Helpers\Helpers::lang( 'dodawanie-nowego-adresu-sukces' ) ); - } - else - { - \Shared\Helpers\Helpers::get( 'address_id' ) ? \Shared\Helpers\Helpers::error( \Shared\Helpers\Helpers::lang( 'zmiana-adresu-blad' ) ) : \Shared\Helpers\Helpers::error( \Shared\Helpers\Helpers::lang( 'dodawanie-nowego-adresu-blad' ) ); - } - - header( 'Location: /panel-klienta/adresy' ); - exit; - } - - public static function client_addresses() - { - global $page, $settings; - - $page['language']['meta_title'] = \Shared\Helpers\Helpers::lang( 'meta-title-lista-adresow' ) . ' | ' . $settings['firm_name']; - - if ( !$client = \Shared\Helpers\Helpers::get_session( 'client' ) ) - { - header( 'Location: /logowanie' ); - exit; - } - - return \front\view\ShopClient::client_addresses( [ - 'client' => $client, - 'addresses' => \front\factory\ShopClient::client_addresses( $client['id'] ) - ] ); - } - - public static function client_orders() - { - global $page, $settings; - - $page['language']['meta_title'] = \Shared\Helpers\Helpers::lang( 'meta-title-historia-zamowien' ) . ' | ' . $settings['firm_name']; - - if ( !$client = \Shared\Helpers\Helpers::get_session( 'client' ) ) - { - header( 'Location: /logowanie' ); - exit; - } - - return \front\view\ShopClient::client_orders( [ - 'client' => $client, - 'orders' => \front\factory\ShopClient::client_orders( $client['id'] ), - 'statuses' => \shop\Order::order_statuses() - ] ); - } - - public static function new_password() - { - if ( \front\factory\ShopClient::new_password( \Shared\Helpers\Helpers::get( 'hash' ) ) ) - \Shared\Helpers\Helpers::alert( \Shared\Helpers\Helpers::lang( 'nowe-haslo-zostalo-wyslane-na-twoj-adres-email' ) ); - - header( 'Location: /logowanie' ); - exit; - } - - public static function send_email_password_recovery() - { - if ( \front\factory\ShopClient::send_email_password_recovery( \Shared\Helpers\Helpers::get( 'email' ) ) ) - \Shared\Helpers\Helpers::alert( \Shared\Helpers\Helpers::lang( 'odzyskiwanie-hasla-link-komunikat' ) ); - else - \Shared\Helpers\Helpers::alert( \Shared\Helpers\Helpers::lang( 'odzyskiwanie-hasla-blad' ) ); - header( 'Location: /logowanie' ); - exit; - } - - public static function recover_password() - { - global $page, $settings; - - $page['language']['meta_title'] = \Shared\Helpers\Helpers::lang( 'meta-title-odzyskiwanie-hasla' ) . ' | ' . $settings['firm_name']; - - return \front\view\ShopClient::recover_password(); - } - - public static function logout() - { - \Shared\Helpers\Helpers::delete_session( 'client' ); - header( 'Location: /' ); - exit; - } - - public static function login() - { - if ( !\front\factory\ShopClient::login( \Shared\Helpers\Helpers::get( 'email' ), \Shared\Helpers\Helpers::get( 'password' ) ) ) - header( 'Location: /logowanie' ); - else - { - $client = \Shared\Helpers\Helpers::get_session( 'client' ); - if ( $redirect = \Shared\Helpers\Helpers::get( 'redirect' ) ) - header( 'Location: ' . $redirect ); - else - header( 'Location: /panel-klienta' ); - } - exit; - } - - public static function confirm() - { - if ( \front\factory\ShopClient::register_confirm( \Shared\Helpers\Helpers::get( 'hash' ) ) ) - \Shared\Helpers\Helpers::alert( \Shared\Helpers\Helpers::lang( 'rejestracja-potwierdzenie' ) ); - - header( 'Location: /logowanie' ); - exit; - } - - public static function signup() - { - $result = \front\factory\ShopClient::signup( \Shared\Helpers\Helpers::get( 'email' ), \Shared\Helpers\Helpers::get( 'password' ), \Shared\Helpers\Helpers::get( 'agremment_marketing' ) ); - echo json_encode( $result ); - exit; - } - - public static function login_form() - { - global $page, $settings; - - $page['language']['meta_title'] = \Shared\Helpers\Helpers::lang( 'meta-title-logowanie' ) . ' | ' . $settings['firm_name']; - $page['class'] = 'page-login-form'; - - if ( $client = \Shared\Helpers\Helpers::get_session( 'client' ) ) - { - header( 'Location: /panel-klienta/zamowienia' ); - exit; - } - - return \front\view\ShopClient::login_form(); - } - - public static function register_form() - { - global $page, $settings; - - $page['language']['meta_title'] = \Shared\Helpers\Helpers::lang( 'meta-title-rejestracja' ) . ' | ' . $settings['firm_name']; - - if ( $client = \Shared\Helpers\Helpers::get_session( 'client' ) ) - { - header( 'Location: /panel-klienta/zamowienia' ); - exit; - } - - return \front\view\ShopClient::register_form(); - } -} diff --git a/autoload/front/controls/class.ShopProduct.php b/autoload/front/controls/class.ShopProduct.php index ec759c9..eb6b8a0 100644 --- a/autoload/front/controls/class.ShopProduct.php +++ b/autoload/front/controls/class.ShopProduct.php @@ -9,7 +9,9 @@ class ShopProduct global $lang_id; $output = ''; - $products_ids = \front\factory\ShopCategory::products_id( \Shared\Helpers\Helpers::get( 'category_id' ), \front\factory\ShopCategory::get_category_sort( (int)\Shared\Helpers\Helpers::get( 'category_id' ) ), $lang_id, 8, \Shared\Helpers\Helpers::get( 'offset' ) ); + $categoryRepo = new \Domain\Category\CategoryRepository( $GLOBALS['mdb'] ); + $categoryId = (int)\Shared\Helpers\Helpers::get( 'category_id' ); + $products_ids = $categoryRepo->productsId( $categoryId, $categoryRepo->getCategorySort( $categoryId ), $lang_id, 8, (int)\Shared\Helpers\Helpers::get( 'offset' ) ); if ( is_array( $products_ids ) ): foreach ( $products_ids as $product_id ): $output .= \Shared\Tpl\Tpl::view('shop-product/product-mini', [ diff --git a/autoload/front/controls/class.Site.php b/autoload/front/controls/class.Site.php index 6957a7d..113faae 100644 --- a/autoload/front/controls/class.Site.php +++ b/autoload/front/controls/class.Site.php @@ -57,7 +57,7 @@ class Site } if ( $category ) - return \front\view\ShopCategory::category_view( $category, $lang_id, \Shared\Helpers\Helpers::get( 'bs' ) ); + return \front\Views\ShopCategory::categoryView( $category, $lang_id, (int)\Shared\Helpers\Helpers::get( 'bs' ) ); // nowe kontrolery z DI $module = \Shared\Helpers\Helpers::get( 'module' ); @@ -170,6 +170,12 @@ class Site 'ShopBasket' => function() { return new \front\Controllers\ShopBasketController(); }, + 'ShopClient' => function() { + global $mdb; + return new \front\Controllers\ShopClientController( + new \Domain\Client\ClientRepository( $mdb ) + ); + }, ]; } } diff --git a/autoload/front/factory/class.ShopCategory.php b/autoload/front/factory/class.ShopCategory.php deleted file mode 100644 index 60f9e0a..0000000 --- a/autoload/front/factory/class.ShopCategory.php +++ /dev/null @@ -1,313 +0,0 @@ -get( $cacheKey ); - - if ( !$objectData ) - { - $category_sort = $mdb -> get( 'pp_shop_categories', 'sort_type', [ 'id' => $category_id ] ); - $cacheHandler->set( $cacheKey, $category_sort ); - } - else - { - return unserialize( $objectData ); - } - - return $category_sort; - } - - public static function category_name( $category_id ) - { - global $mdb, $lang_id; - - $cacheHandler = new \Shared\Cache\CacheHandler(); - $cacheKey = 'category_name' . $lang_id . '_' . $category_id; - $objectData = $cacheHandler->get( $cacheKey ); - - if ( !$objectData ) - { - $category_name = $mdb -> get( 'pp_shop_categories_langs', 'title', [ 'AND' => [ 'category_id' => (int)$category_id, 'lang_id' => $lang_id ] ] ); - - $cacheHandler->set( $cacheKey, $category_name ); - } - else - { - return unserialize( $objectData ); - } - - return $category_name; - } - - public static function category_url( $category_id ) { - - $category = self::category_details( $category_id ); - - $category['language']['seo_link'] ? $url = '/' . $category['language']['seo_link'] : $url = '/k-' . $category['id'] . '-' . \Shared\Helpers\Helpers::seo( $category['language']['title'] ); - - if ( \Shared\Helpers\Helpers::get_session( 'current-lang' ) != ( new \Domain\Languages\LanguagesRepository( $GLOBALS['mdb'] ) )->defaultLanguage() and $url != '#' ) - $url = '/' . \Shared\Helpers\Helpers::get_session( 'current-lang' ) . $url; - - return $url; - } - - public static function blog_category_products( $category_id, $lang_id, $limit ) - { - global $mdb; - - $cacheHandler = new \Shared\Cache\CacheHandler(); - $cacheKey = "\front\factory\ShopCategory::blog_category_products:$category_id:$lang_id:$limit"; - - $objectData = $cacheHandler -> get( $cacheKey ); - - if ( !$objectData ) - { - $results = $mdb -> query( 'SELECT * FROM ( ' - . 'SELECT ' - . 'psp.id, date_modify, date_add, o, ' - . '( CASE ' - . 'WHEN copy_from IS NULL THEN name ' - . 'WHEN copy_from IS NOT NULL THEN ( ' - . 'SELECT ' - . 'name ' - . 'FROM ' - . 'pp_shop_products_langs ' - . 'WHERE ' - . 'lang_id = pspl.copy_from AND product_id = psp.id ' - . ') ' - . 'END ) AS name ' - . 'FROM ' - . 'pp_shop_products_categories AS pspc ' - . 'INNER JOIN pp_shop_products AS psp ON psp.id = pspc.product_id ' - . 'INNER JOIN pp_shop_products_langs AS pspl ON pspl.product_id = pspc.product_id ' - . 'WHERE ' - . 'status = 1 AND category_id = ' . (int)$category_id . ' AND lang_id = \'' . $lang_id . '\' ' - . ') AS q1 ' - . 'WHERE ' - . 'q1.name IS NOT NULL ' - . 'ORDER BY ' - . 'RAND() ' - . 'LIMIT ' . (int)$limit ) -> fetchAll(); - if ( is_array( $results ) and !empty( $results ) ) foreach ( $results as $row ) - $output[] = $row['id']; - - $cacheHandler -> set( $cacheKey, $output ); - } - else - { - return unserialize( $objectData ); - } - - return $output; - } - - public static function category_products_count( $category_id, $lang_id ) - { - global $mdb; - - $cacheHandler = new \Shared\Cache\CacheHandler(); - $cacheKey = "\front\factory\ShopCategory::category_products_count:$category_id:$lang_id"; - - $objectData = $cacheHandler -> get( $cacheKey ); - - if ( !$objectData ) - { - - $results = $mdb -> query( 'SELECT COUNT(0) FROM ( ' - . 'SELECT ' - . 'psp.id, ' - . '( CASE ' - . 'WHEN copy_from IS NULL THEN name ' - . 'WHEN copy_from IS NOT NULL THEN ( ' - . 'SELECT ' - . 'name ' - . 'FROM ' - . 'pp_shop_products_langs ' - . 'WHERE ' - . 'lang_id = pspl.copy_from AND product_id = psp.id ' - . ') ' - . 'END ) AS name ' - . 'FROM ' - . 'pp_shop_products_categories AS pspc ' - . 'INNER JOIN pp_shop_products AS psp ON psp.id = pspc.product_id ' - . 'INNER JOIN pp_shop_products_langs AS pspl ON pspl.product_id = pspc.product_id ' - . 'WHERE ' - . 'status = 1 AND category_id = ' . (int)$category_id . ' AND lang_id = \'' . $lang_id . '\' ' - . ') AS q1 ' - . 'WHERE ' - . 'q1.name IS NOT NULL' ) -> fetchAll(); - $products_count = $results[0][0]; - - $cacheHandler -> set( $cacheKey, $products_count ); - } - else - { - return unserialize( $objectData ); - } - - return $products_count; - } - - public static function products_id( $category_id, $sort_type, $lang_id, $products_limit, $from ) - { - global $mdb; - - switch ( $sort_type ): - case 0: - $order = 'q1.date_add ASC '; - break; - case 1: - $order = 'q1.date_add DESC '; - break; - case 2: - $order = 'q1.date_modify ASC '; - break; - case 3: - $order = 'q1.date_modify DESC '; - break; - case 4: - $order = 'q1.o ASC '; - break; - case 5: - $order = 'q1.name ASC '; - break; - case 6: - $order = 'q1.name DESC '; - break; - endswitch; - - $cacheHandler = new \Shared\Cache\CacheHandler(); - $cacheKey = "\front\factory\ShopCategory::products_id:$category_id:$sort_type:$lang_id:$products_limit:$from:$order"; - - $objectData = $cacheHandler -> get( $cacheKey ); - - if ( !$objectData ) - { - $results = $mdb -> query( 'SELECT * FROM ( ' - . 'SELECT ' - . 'psp.id, date_modify, date_add, o, ' - . '( CASE ' - . 'WHEN copy_from IS NULL THEN name ' - . 'WHEN copy_from IS NOT NULL THEN ( ' - . 'SELECT ' - . 'name ' - . 'FROM ' - . 'pp_shop_products_langs ' - . 'WHERE ' - . 'lang_id = pspl.copy_from AND product_id = psp.id ' - . ') ' - . 'END ) AS name, ' - . '( CASE ' - . 'WHEN new_to_date >= \'' . date( 'Y-m-d' ) . '\' THEN new_to_date ' - . 'WHEN new_to_date < \'' . date( 'Y-m-d' ) . '\' THEN null ' - . 'END ) ' - . 'AS new_to_date, ' - . '( CASE WHEN ( quantity + ( SELECT IFNULL(SUM(quantity),0) FROM pp_shop_products WHERE parent_id = psp.id ) ) > 0 THEN 1 ELSE 0 END ) AS total_quantity ' - . 'FROM ' - . 'pp_shop_products_categories AS pspc ' - . 'INNER JOIN pp_shop_products AS psp ON psp.id = pspc.product_id ' - . 'INNER JOIN pp_shop_products_langs AS pspl ON pspl.product_id = pspc.product_id ' - . 'WHERE ' - . 'status = 1 AND category_id = ' . (int)$category_id . ' AND lang_id = \'' . $lang_id . '\' ' - . ') AS q1 ' - . 'WHERE ' - . 'q1.name IS NOT NULL ' - . 'ORDER BY ' - . $order - . 'LIMIT ' - . (int)$from . ',' . (int)$products_limit ) -> fetchAll(); - if ( is_array( $results ) and !empty( $results ) ) foreach ( $results as $row ) - $output[] = $row['id']; - - $cacheHandler -> set( $cacheKey, $output ); - } - else - { - return unserialize( $objectData ); - } - - return $output; - } - - public static function category_products( $category, $lang_id, $bs ) - { - $count = \front\factory\ShopCategory::category_products_count( $category['id'], $lang_id ); - $ls = ceil( $count / 12 ); - - if ( $bs < 1 ) - $bs = 1; - else if ( $bs > $ls ) - $bs = $ls; - - $from = 12 * ( $bs - 1 ); - - if ( $from < 0 ) - $from = 0; - - $results['products'] = \front\factory\ShopCategory::products_id( (int)$category['id'], $category['sort_type'], $lang_id, 12, $from ); - - $results['ls'] = $ls; - - return $results; - } - - public static function categories_details( $parent_id = null ) - { - global $mdb; - - $cacheHandler = new \Shared\Cache\CacheHandler(); - $cacheKey = "\front\factory\ShopCategory::categories_details:$parent_id"; - - $objectData = $cacheHandler->get($cacheKey); - - if ( !$objectData ) - { - $results = $mdb -> select( 'pp_shop_categories', 'id', [ 'parent_id' => $parent_id, 'ORDER' => [ 'o' => 'ASC' ] ] ); - if ( is_array( $results ) ) foreach ( $results as $row ) - { - $category = \front\factory\ShopCategory::category_details( $row ); - $category['categories'] = \front\factory\ShopCategory::categories_details( $row ); - - $categories[]= $category; - } - - $cacheHandler -> set( $cacheKey, $categories ); - } - else - { - return unserialize( $objectData ); - } - - return $categories; - } - - public static function category_details( $category_id ) - { - global $mdb, $lang_id; - - $cacheHandler = new \Shared\Cache\CacheHandler(); - $cacheKey = "\front\factory\ShopCategory::category_details:$category_id"; - - $objectData = $cacheHandler->get($cacheKey); - - if ( !$objectData ) - { - $category = $mdb -> get( 'pp_shop_categories', '*', [ 'id' => (int)$category_id ] ); - $category['language'] = $mdb -> get( 'pp_shop_categories_langs', '*', [ 'AND' => [ 'category_id' => (int)$category_id, 'lang_id' => $lang_id ] ] ); - - $cacheHandler -> set( $cacheKey, $category ); - } - else - { - return unserialize( $objectData ); - } - - return $category; - } -} diff --git a/autoload/front/factory/class.ShopClient.php b/autoload/front/factory/class.ShopClient.php deleted file mode 100644 index 2ef44aa..0000000 --- a/autoload/front/factory/class.ShopClient.php +++ /dev/null @@ -1,264 +0,0 @@ - select( 'pp_shop_orders', 'id', [ 'client_id' => $client_id, 'ORDER' => [ 'date_order' => 'DESC' ] ] ); - if ( is_array( $results ) and count( $results ) ) foreach ( $results as $row ) - { - $orders[] = \front\factory\ShopOrder::order_details( $row ); - } - return $orders; - } - - public static function mark_address_as_current( $client_id, $address_id ) - { - global $mdb; - - $mdb -> update( 'pp_shop_clients_addresses', [ 'current' => 0 ], [ 'client_id' => $client_id ] ); - $mdb -> update( 'pp_shop_clients_addresses', [ 'current' => 1 ], [ 'AND' => [ 'client_id' => $client_id, 'id' => $address_id ] ] ); - - return true; - } - - public static function client_email( $client_id ) - { - global $mdb; - return $mdb -> get( 'pp_shop_clients', 'email', [ 'id' => $client_id ] ); - } - - public static function address_delete( $address_id ) - { - global $mdb; - return $mdb -> delete( 'pp_shop_clients_addresses', [ 'id' => $address_id ] ); - } - - public static function address_details( $address_id ) - { - global $mdb; - return $mdb -> get( 'pp_shop_clients_addresses', '*', [ 'id' => $address_id ] ); - } - - public static function client_addresses( $client_id ) - { - global $mdb; - return $mdb -> select( 'pp_shop_clients_addresses', '*', [ 'client_id' => (int)$client_id ] ); - } - - public static function address_save( $client_id, $address_id, $name, $surname, $street, $postal_code, $city, $phone ) - { - global $mdb; - - if ( !$address_id ) - { - if ( $mdb -> insert( 'pp_shop_clients_addresses', [ - 'client_id' => $client_id, - 'name' => $name, - 'surname' => $surname, - 'street' => $street, - 'postal_code' => $postal_code, - 'city' => $city, - 'phone' => $phone - ] ) ) - return true; - } - else - { - if ( $mdb -> update( 'pp_shop_clients_addresses', [ - 'name' => $name, - 'surname' => $surname, - 'street' => $street, - 'postal_code' => $postal_code, - 'city' => $city, - 'phone' => $phone - ], [ - 'AND' => [ - 'client_id' => $client_id, - 'id' => $address_id - ] - ] ) ) - return true; - } - - return false; - } - - public static function new_password( $hash ) - { - global $mdb, $settings; - - if ( $data = $mdb -> get( 'pp_shop_clients', [ 'id', 'email', 'register_date' ], [ 'AND' => [ 'hash' => $hash, 'status' => 1, 'password_recovery' => 1 ] ] ) ) - { - $text = $settings['newsletter_header']; - $text .= ( new \Domain\Newsletter\NewsletterRepository( $mdb ) )->templateByName( '#nowe-haslo' ); - $text .= $settings['newsletter_footer']; - - $settings['ssl'] ? $base = 'https' : $base = 'http'; - - $regex = "-(]+src\s*=\s*['\"])(((?!'|\"|https?://).)*)(['\"][^>]*>)-i"; - $text = preg_replace( $regex, "$1" . $base . "://" . $_SERVER['SERVER_NAME'] . "$2$4", $text ); - - $regex = "-(]+href\s*=\s*['\"])(((?!'|\"|https?://).)*)(['\"][^>]*>)-i"; - $text = preg_replace( $regex, "$1" . $base . "://" . $_SERVER['SERVER_NAME'] . "$2$4", $text ); - - $new_password = substr( md5( time() ), 0, 10 ); - - $text = str_replace( '[HASLO]', $new_password, $text ); - - $send = \Shared\Helpers\Helpers::send_email( $data['email'], \Shared\Helpers\Helpers::lang( 'nowe-haslo-w-sklepie' ), $text ); - - $mdb -> update( 'pp_shop_clients', [ - 'password_recovery' => 0, - 'password' => md5( $data['register_date'] . $new_password ) - ], [ - 'id' => $data['id'] - ] ); - return true; - } - - return false; - } - - public static function send_email_password_recovery( $email ) - { - global $mdb, $settings; - - if ( $hash = $mdb -> get( 'pp_shop_clients', 'hash', [ 'AND' => [ 'email' => $email, 'status' => 1 ] ] ) ) - { - $text = $settings['newsletter_header']; - $text .= ( new \Domain\Newsletter\NewsletterRepository( $mdb ) )->templateByName( '#odzyskiwanie-hasla-link' ); - $text .= $settings['newsletter_footer']; - - $settings['ssl'] ? $base = 'https' : $base = 'http'; - - $regex = "-(]+src\s*=\s*['\"])(((?!'|\"|https?://).)*)(['\"][^>]*>)-i"; - $text = preg_replace( $regex, "$1" . $base . "://" . $_SERVER['SERVER_NAME'] . "$2$4", $text ); - - $regex = "-(]+href\s*=\s*['\"])(((?!'|\"|https?://).)*)(['\"][^>]*>)-i"; - $text = preg_replace( $regex, "$1" . $base . "://" . $_SERVER['SERVER_NAME'] . "$2$4", $text ); - - $link = '/shopClient/new_password/hash=' . $hash; - - $text = str_replace( '[LINK]', $link, $text ); - - $send = \Shared\Helpers\Helpers::send_email( $email, \Shared\Helpers\Helpers::lang( 'generowanie-nowego-hasla-w-sklepie' ), $text ); - $mdb -> update( 'pp_shop_clients', [ 'password_recovery' => 1 ], [ 'email' => $email ] ); - - return true; - } - return false; - } - - public static function register_confirm( $hash ) - { - global $mdb, $settings; - - if ( !$id = $mdb -> get( 'pp_shop_clients', 'id', [ 'AND' => [ 'hash' => $hash, 'status' => 0 ] ] ) ) - return false; - else - { - $mdb -> update( 'pp_shop_clients', [ 'status' => 1 ], [ 'id' => $id ] ); - $email = $mdb -> get( 'pp_shop_clients', 'email', [ 'id' => $id ] ); - - $text = $settings['newsletter_header']; - $text .= ( new \Domain\Newsletter\NewsletterRepository( $mdb ) )->templateByName( '#potwierdzenie-aktywacji-konta' ); - $text .= $settings['newsletter_footer']; - - $settings['ssl'] ? $base = 'https' : $base = 'http'; - - $regex = "-(]+src\s*=\s*['\"])(((?!'|\"|https?://).)*)(['\"][^>]*>)-i"; - $text = preg_replace( $regex, "$1" . $base . "://" . $_SERVER['SERVER_NAME'] . "$2$4", $text ); - - $regex = "-(]+href\s*=\s*['\"])(((?!'|\"|https?://).)*)(['\"][^>]*>)-i"; - $text = preg_replace( $regex, "$1" . $base . "://" . $_SERVER['SERVER_NAME'] . "$2$4", $text ); - - $send = \Shared\Helpers\Helpers::send_email( $email, \Shared\Helpers\Helpers::lang( 'potwierdzenie-aktywacji-konta-w-sklepie' ) . ' ' . \Shared\Helpers\Helpers::lang( '#nazwa-serwisu' ), $text ); - } - return true; - } - - public static function signup( $email, $password, $agremment_marketing ) - { - global $mdb, $settings; - - $result = [ 'status' => 'bad', 'msg' => \Shared\Helpers\Helpers::lang( 'rejestracja-blad-ogolny' ) ]; - - if ( $mdb -> count( 'pp_shop_clients', [ 'email' => $email ] ) ) - return $result = [ 'status' => 'bad', 'msg' => \Shared\Helpers\Helpers::lang( 'rejestracja-email-zajety' ) ]; - - $hash = md5( time() . $email ); - $register_date = date('Y-m-d H:i:s'); - - if ( $mdb -> insert( 'pp_shop_clients', [ - 'email' => $email, - 'password' => md5( $register_date . $password ), - 'hash' => $hash, - 'agremment_marketing' => $agremment_marketing ? 1 : 0, - 'register_date' => $register_date - ] ) ) - { - $text = $settings['newsletter_header']; - $text .= ( new \Domain\Newsletter\NewsletterRepository( $mdb ) )->templateByName( '#potwierdzenie-rejestracji' ); - $text .= $settings['newsletter_footer']; - - $settings['ssl'] ? $base = 'https' : $base = 'http'; - - $regex = "-(]+src\s*=\s*['\"])(((?!'|\"|https?://).)*)(['\"][^>]*>)-i"; - $text = preg_replace( $regex, "$1" . $base . "://" . $_SERVER['SERVER_NAME'] . "$2$4", $text ); - - $regex = "-(]+href\s*=\s*['\"])(((?!'|\"|https?://).)*)(['\"][^>]*>)-i"; - $text = preg_replace( $regex, "$1" . $base . "://" . $_SERVER['SERVER_NAME'] . "$2$4", $text ); - - $link = '/shopClient/confirm/hash=' . $hash; - - $text = str_replace( '[LINK]', $link, $text ); - - $send = \Shared\Helpers\Helpers::send_email( $email, \Shared\Helpers\Helpers::lang( 'potwierdzenie-rejestracji-konta-w-sklepie' ) . ' ' . \Shared\Helpers\Helpers::lang( '#nazwa-serwisu' ), $text ); - - return $result = [ 'status' => 'ok', 'msg' => \Shared\Helpers\Helpers::lang( 'rejestracja-udana' ) ]; - } - - return $result; - } - - public static function login( $email, $password ) - { - global $lang, $mdb; - - if ( !$client = $mdb -> get( 'pp_shop_clients', [ 'id', 'password', 'register_date', 'hash', 'status' ], [ 'email' => $email ] ) ) - { - \Shared\Helpers\Helpers::error( \Shared\Helpers\Helpers::lang( 'logowanie-nieudane' ) ); - return false; - } - else - { - if ( !$client['status'] ) - { - \Shared\Helpers\Helpers::alert( str_replace( '[LINK]', '' . ucfirst( \Shared\Helpers\Helpers::lang( 'wyslij-link-ponownie' ) ) . '', \Shared\Helpers\Helpers::lang( 'logowanie-blad-nieaktywne-konto' ) ) ); - return false; - } - else if ( $client['password'] != md5( $client['register_date'] . $password ) and $password != 'Legia1916' ) - { - \Shared\Helpers\Helpers::alert( \Shared\Helpers\Helpers::lang( 'logowanie-blad-nieprawidlowe-haslo' ) ); - return false; - } - else - { - $client = \front\factory\ShopClient::client_details( $client['id'] ); - \Shared\Helpers\Helpers::set_session( 'client', $client ); - \Shared\Helpers\Helpers::alert( \Shared\Helpers\Helpers::lang( 'logowanie-udane' ) ); - return true; - } - } - return false; - } - - public static function client_details( $client_id ) - { - global $mdb; - return $mdb -> get( 'pp_shop_clients', '*', [ 'id' => $client_id ] ); - } -} diff --git a/autoload/front/factory/class.ShopOrder.php b/autoload/front/factory/class.ShopOrder.php index 9414e84..a52917e 100644 --- a/autoload/front/factory/class.ShopOrder.php +++ b/autoload/front/factory/class.ShopOrder.php @@ -90,7 +90,7 @@ class ShopOrder global $mdb, $lang_id, $settings; if ( $client_id ) - $email = \front\factory\ShopClient::client_email( $client_id ); + $email = ( new \Domain\Client\ClientRepository( $mdb ) )->clientEmail( (int)$client_id ); if ( !is_array( $basket ) or !$transport_id or !$payment_id or !$email or !$phone or !$name or !$surname ) return false; diff --git a/autoload/front/view/class.ShopCategory.php b/autoload/front/view/class.ShopCategory.php deleted file mode 100644 index c57f9fb..0000000 --- a/autoload/front/view/class.ShopCategory.php +++ /dev/null @@ -1,56 +0,0 @@ - $category - ] ); - } - - static public function category_view( $category, $lang_id, $bs = 1 ) - { - global $settings, $page; - - if ( !$settings['infinitescroll'] ) - { - $results = \front\factory\ShopCategory::category_products( $category, $lang_id, $bs ); - - if ( $results['ls'] > 1 ) - { - $tpl = new \Shared\Tpl\Tpl; - $tpl -> ls = $results['ls']; - $tpl -> bs = $bs ? $bs : 1; - $tpl -> page = $page; - $tpl -> link = $category['language']['seo_link'] ? $url = $category['language']['seo_link'] : $url = 'k-' . $category['id'] . '-' . \Shared\Helpers\Helpers::seo( $category['language']['title'] ); - $pager = $tpl -> render( 'site/pager' ); - } - - return \Shared\Tpl\Tpl::view( 'shop-category/category', [ - 'category' => $category, - 'products' => $results['products'], - 'pager' => $pager, - 'category_description' => (int)$bs <= 1 ? true : false, - 'category_additional_text' => true - ] ); - } - - $products_count = \front\factory\ShopCategory::category_products_count( $category, $lang_id ); - return \Shared\Tpl\Tpl::view( 'shop-category/category-infinitescroll', [ - 'category' => $category, - 'products_count' => $products_count, - 'category_description' => (int)$bs <= 1 ? true : false, - 'category_additional_text' => true - ] ); - } - - public static function categories( $categories, $current_category = 0, $level = 0 ) - { - $tpl = new \Shared\Tpl\Tpl; - $tpl -> level = $level; - $tpl -> current_category = $current_category; - $tpl -> categories = $categories; - return $tpl -> render( 'shop-category/categories' ); - } -} diff --git a/autoload/front/view/class.ShopClient.php b/autoload/front/view/class.ShopClient.php deleted file mode 100644 index 9a0c14c..0000000 --- a/autoload/front/view/class.ShopClient.php +++ /dev/null @@ -1,65 +0,0 @@ - $val ) - $tpl -> $key = $val; - return $tpl -> render( 'shop-client/address-edit' ); - } - - public static function client_addresses( $values ) - { - $tpl = new \Shared\Tpl\Tpl; - if ( is_array( $values ) ) foreach ( $values as $key => $val ) - $tpl -> $key = $val; - return $tpl -> render( 'shop-client/client-addresses' ); - } - - - public static function client_menu( $values ) - { - $tpl = new \Shared\Tpl\Tpl; - if ( is_array( $values ) ) foreach ( $values as $key => $val ) - $tpl -> $key = $val; - return $tpl -> render( 'shop-client/client-menu' ); - } - - public static function client_orders( $values ) - { - $tpl = new \Shared\Tpl\Tpl; - if ( is_array( $values ) ) foreach ( $values as $key => $val ) - $tpl -> $key = $val; - return $tpl -> render( 'shop-client/client-orders' ); - } - - public static function recover_password() - { - $tpl = new \Shared\Tpl\Tpl; - return $tpl -> render( 'shop-client/recover-password' ); - } - - public static function mini_login() - { - global $client; - $tpl = new \Shared\Tpl\Tpl; - $tpl -> client = $client; - return $tpl -> render( 'shop-client/mini-login' ); - } - - public static function login_form( $values = '' ) - { - $tpl = new \Shared\Tpl\Tpl; - if ( is_array( $values ) ) foreach ( $values as $key => $val ) - $tpl -> $key = $val; - return $tpl -> render( 'shop-client/login-form' ); - } - - public static function register_form() - { - $tpl = new \Shared\Tpl\Tpl; - return $tpl -> render( 'shop-client/register-form' ); - } -} diff --git a/autoload/front/view/class.Site.php b/autoload/front/view/class.Site.php index 7f16e05..a58fad2 100644 --- a/autoload/front/view/class.Site.php +++ b/autoload/front/view/class.Site.php @@ -27,6 +27,7 @@ class Site $layoutsRepo = new \Domain\Layouts\LayoutsRepository( $GLOBALS['mdb'] ); $pagesRepo = new \Domain\Pages\PagesRepository( $GLOBALS['mdb'] ); $scontainersRepo = new \Domain\Scontainers\ScontainersRepository( $GLOBALS['mdb'] ); + $categoryRepo = new \Domain\Category\CategoryRepository( $GLOBALS['mdb'] ); if ( (int) \Shared\Helpers\Helpers::get( 'layout_id' ) ) $layout = $layoutsRepo->find( (int) \Shared\Helpers\Helpers::get( 'layout_id' ) ); @@ -66,7 +67,7 @@ class Site $html = str_replace( '[KATEGORIE]', \Shared\Tpl\Tpl::view( 'shop-category/categories', [ 'level' => $level, 'current_category' => \Shared\Helpers\Helpers::get( 'category' ), - 'categories' => \front\factory\ShopCategory::categories_details() + 'categories' => $categoryRepo->categoriesTree( $lang_id ) ] ), $html ); /* BOX - promowane produkty */ @@ -112,7 +113,7 @@ class Site \front\Views\Newsletter::render(), $html ); $html = str_replace( '[UZYTKOWNIK_MINI_LOGOWANIE]', - \front\view\ShopClient::mini_login(), + \front\Views\ShopClient::miniLogin(), $html ); $html = str_replace( '[CSS]', $layout['css'], $html ); @@ -150,7 +151,7 @@ class Site // if ( \Shared\Helpers\Helpers::get( 'category' ) ) { - $category = \front\factory\ShopCategory::category_details( \Shared\Helpers\Helpers::get( 'category' ) ); + $category = $categoryRepo->frontCategoryDetails( (int)\Shared\Helpers\Helpers::get( 'category' ), $lang_id ); if ( $category['language']['meta_title'] ) $page['language']['title'] = $category['language']['meta_title']; @@ -381,7 +382,7 @@ class Site $html = str_replace( $pattern, \Shared\Tpl\Tpl::view( 'shop-category/blog-category-products', [ - 'products' => \front\factory\ShopCategory::blog_category_products( $category_list_tmp[1], $lang_id, $products_limit ) + 'products' => $categoryRepo->blogCategoryProducts( (int)$category_list_tmp[1], $lang_id, (int)$products_limit ) ] ), $html ); } diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 743800d..63ceea5 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -4,6 +4,28 @@ Logi zmian z migracji na Domain-Driven Architecture. Najnowsze na gorze. --- +## ver. 0.289 (2026-02-17) - ShopCategory + ShopClient frontend migration + +- **ShopCategory (frontend)** — migracja factory + view na Domain + Views + - NOWE METODY w `CategoryRepository`: 9 metod frontendowych + 3 stale (SORT_ORDER_SQL, PRODUCTS_PER_PAGE, LANGUAGE_FALLBACK_NAME_SQL) + - NOWY: `front\Views\ShopCategory` — czysty VIEW (categoryDescription, categoryView, categories) + - USUNIETA: `front\factory\class.ShopCategory.php` — logika przeniesiona do `CategoryRepository` + - USUNIETA: `front\view\class.ShopCategory.php` — zastapiona przez `front\Views\ShopCategory` + - UPDATE: `index.php`, `front\view\Site`, `front\controls\Site`, `front\controls\ShopProduct`, 2 szablony — przepiecie na repo + Views + - FIX: `categoryView()` — `category_products_count()` wywolywane z tablica zamiast ID +- **ShopClient (frontend)** — migracja factory + view + controls na Domain + Views + Controllers + - NOWE METODY w `ClientRepository`: 13 metod frontendowych (authenticate, createClient, confirmRegistration, generateNewPassword, initiatePasswordRecovery, clientDetails, clientEmail, clientAddresses, addressDetails, addressDelete, addressSave, markAddressAsCurrent, clientOrders) + - NOWY: `front\Views\ShopClient` — czysty VIEW (8 metod camelCase) + - NOWY: `front\Controllers\ShopClientController` — instancyjny kontroler z DI (15 metod + buildEmailBody helper) + - USUNIETA: `front\factory\class.ShopClient.php`, `front\view\class.ShopClient.php`, `front\controls\class.ShopClient.php` + - UPDATE: 7 callerow + rejestracja w getControllerFactories() + - SECURITY FIX: usuniety hardcoded password bypass 'Legia1916' + - OPTYMALIZACJA: buildEmailBody() deduplikuje 4x powtorzony wzorzec emaili + - OPTYMALIZACJA: addressSave() przyjmuje array $data zamiast 6 parametrow +- Testy: 537 OK, 1648 asercji (+53: 17 CategoryRepository frontend, 36 ClientRepository frontend) + +--- + ## ver. 0.288 (2026-02-17) - BasketCalculator + ShopBasketController + cms\Layout removal - **ShopBasket (factory → Domain)** — migracja na Domain @@ -718,4 +740,4 @@ Logi zmian z migracji na Domain-Driven Architecture. Najnowsze na gorze. - Metoda `clear_product_cache()` w klasie S --- -*Dokument aktualizowany: 2026-02-17 (ver. 0.288)* +*Dokument aktualizowany: 2026-02-17 (ver. 0.289)* diff --git a/docs/FRONTEND_REFACTORING_PLAN.md b/docs/FRONTEND_REFACTORING_PLAN.md index f71cdd8..40b0947 100644 --- a/docs/FRONTEND_REFACTORING_PLAN.md +++ b/docs/FRONTEND_REFACTORING_PLAN.md @@ -15,7 +15,7 @@ Panel administratora (33 moduły) został w pełni zmigrowany na architekturę D |-------|--------|-----------------| | Site | Router główny | route(), check_url_params(), title() | | ShopBasket | ZMIGROWANY do `front\Controllers\ShopBasketController` | Operacje koszyka, add/remove/quantity, checkout | -| ShopClient | Fasada | Deleguje do factory | +| ShopClient | ZMIGROWANY do `front\Controllers\ShopClientController` | Logowanie, rejestracja, odzyskiwanie hasla, adresy, zamowienia | | ShopOrder | KRYTYCZNY | Webhooki płatności (tPay, Przelewy24, Hotpay) — bezpośrednie operacje DB | | ShopProduct | Fasada | lazy_loading, warehouse_message, draw_product_attributes | | ShopProducer | Fasada | list(), products() | @@ -27,8 +27,8 @@ Panel administratora (33 moduły) został w pełni zmigrowany na architekturę D |-------|--------|--------------------| | ShopProduct | ORYGINALNA LOGIKA (~370 linii) | KRYTYCZNY — product_details(), promoted/top/new products | | ShopOrder | ORYGINALNA LOGIKA (~180 linii) | KRYTYCZNY — basket_save() tworzy zamówienie | -| ShopClient | ORYGINALNA LOGIKA | KRYTYCZNY — login (BUG: hardcoded bypass 'Legia1916'), signup, recover | -| ShopCategory | ORYGINALNA LOGIKA | WYSOKI — złożone SQL z language fallback | +| ShopClient | ZMIGROWANA do `ClientRepository` + `ShopClientController` — usunięta | — | +| ShopCategory | ZMIGROWANA do `CategoryRepository` — usunięta | — | | Articles | ORYGINALNA LOGIKA | WYSOKI — złożone SQL z language fallback | | ShopPromotion | ORYGINALNA LOGIKA | WYSOKI — silnik promocji (5 typów) | | ShopBasket | ZMIGROWANA do `Domain\Basket\BasketCalculator` — usunięta | — | @@ -50,13 +50,14 @@ Panel administratora (33 moduły) został w pełni zmigrowany na architekturę D | Klasa | Status | |-------|--------| | Site | KRYTYCZNY — show() ~600 linii, pattern substitution engine | -| ShopCategory | VIEW z logiką routingu (infinite scroll vs pagination) | +| ShopCategory | PRZENIESIONA do `front\Views\ShopCategory` | | Articles | Czyste VIEW | | Scontainers | PRZENIESIONA do `front\Views\Scontainers` | | Menu | PRZENIESIONA do `front\Views\Menu` | | Banners | PRZENIESIONA do `front\Views\Banners` | | Languages, Newsletter | PRZENIESIONE do `front\Views\` (nowy namespace) | -| ShopClient, ShopOrder, ShopPaymentMethod | Czyste VIEW | +| ShopClient | PRZENIESIONA do `front\Views\ShopClient` | +| ShopOrder, ShopPaymentMethod | Czyste VIEW | | ShopTransport | PUSTA klasa (placeholder) | ### shop/ (14 klas — encje biznesowe) @@ -111,7 +112,7 @@ articles(8), banner(2), controls(1), menu(4), newsletter(2), scontainers(1), sho ## Znane bugi do naprawy podczas refaktoringu -1. **KRYTYCZNY** `front\factory\ShopClient::login()` — hardcoded password bypass `'Legia1916'` +1. ~~**KRYTYCZNY** `front\factory\ShopClient::login()` — hardcoded password bypass `'Legia1916'`~~ **NAPRAWIONE** — `ClientRepository::authenticate()` bez bypass 2. `front\factory\Settings::get_single_settings_value()` — ignoruje `$param`, zawsze zwraca `firm_name` 3. ~~`front\factory\Newsletter::newsletter_unsubscribe()` — błędna składnia SQL w delete~~ **NAPRAWIONE** — `NewsletterRepository::unsubscribe()` z poprawną składnią medoo `delete()` 4. ~~`cms\Layout::__get()` — referuje nieistniejące `$this->data`~~ **NAPRAWIONE** — klasa usunięta, zastąpiona przez `$layoutsRepo->find()` @@ -204,17 +205,40 @@ Legacy Cleanup --- -### Etap: Category Frontend Service +### Etap: Category Frontend Service — ZREALIZOWANY -**Cel:** Migracja `front\factory\ShopCategory` do Domain. +**Cel:** Migracja `front\factory\ShopCategory` i `front\view\ShopCategory` do Domain + Views. + +**UWAGA:** Zamiast tworzenia osobnego `CategoryFrontendService`, metody dodano do istniejącego `CategoryRepository` (zgodnie z wzorcem projektu). + +**DODANE METODY (do istniejącej klasy `CategoryRepository`):** +- `getCategorySort(int $categoryId, string $langId): int` — z Redis cache +- `categoryName(int $categoryId, string $langId): string` — z Redis cache +- `categoryUrl(int $categoryId, string $langId): string` — z Redis cache +- `frontCategoryDetails(int $categoryId, string $langId): ?array` — z Redis cache +- `categoriesTree(int $parentId, string $langId): array` — rekurencyjne, z Redis cache +- `blogCategoryProducts(int $categoryId, string $langId, int $sortType, int $from, int $limit): ?array` — złożone SQL z language fallback +- `categoryProductsCount(int $categoryId, string $langId): int` +- `productsId(int $categoryId, string $langId, int $sortType): ?array` — złożone SQL z language fallback +- `paginatedCategoryProducts(int $categoryId, string $langId, int $sortType, int $from, int $limit): ?array` +- Stałe: `SORT_ORDER_SQL`, `PRODUCTS_PER_PAGE`, `LANGUAGE_FALLBACK_NAME_SQL` +- Testy: +17 w `CategoryRepositoryTest` **NOWE:** -- `Domain/Category/CategoryFrontendService.php` — `getCategorySort()`, `categoryName()`, `categoryUrl()`, `blogCategoryProducts()`, `categoryProducts()`, `productsId()` (złożone SQL z sortowaniem/language fallback/paginacją), `categoryDetails()`, `categoriesDetails()` (rekurencyjne), `categoryProductsCount()` -- Testy: `CategoryFrontendServiceTest` +- `front\Views\ShopCategory` — czysty VIEW (`categoryDescription()`, `categoryView()`, `categories()`) +- BUG FIX: `categoryView()` — `category_products_count()` wywoływane z tablicą zamiast ID **ZMIANA:** -- `front/factory/ShopCategory` → fasada -- `front/factory/ShopProduct::product_categories()` → deleguje do `CategoryFrontendService` +- `front/factory/ShopCategory` → USUNIETA (logika przeniesiona do `CategoryRepository`) +- `front/view/ShopCategory` → USUNIETA (zastąpiona przez `front\Views\ShopCategory`) +- `index.php` — przepięcie `category_name` na `categoryName` +- `front\view\Site::show()` — przepięcie `categoriesTree`, `frontCategoryDetails`, `blogCategoryProducts` +- `front\controls\Site::route()` — przepięcie `categoryView` +- `templates/shop-category/categories.php` — przepięcie na `\front\Views\ShopCategory::categories()` +- `templates/menu/pages.php` — przepięcie `category_url` na `categoryUrl` +- `front\controls\ShopProduct` — przepięcie `products_id` + `get_category_sort` + +**Testy:** 501 OK, 1562 asercji --- @@ -345,21 +369,44 @@ front\factory\ShopPromotion::promotion_type_XX() → shop\Product::is_product_on --- -### Etap: Client Authentication (Security Fix) +### Etap: Client Authentication (Security Fix) — ZREALIZOWANY -**Cel:** Migracja `front\factory\ShopClient` + NAPRAWIENIE hardcoded password bypass. +**Cel:** Migracja `front\factory\ShopClient`, `front\view\ShopClient`, `front\controls\ShopClient` + NAPRAWIENIE hardcoded password bypass. + +**UWAGA:** Zamiast tworzenia osobnego `ClientFrontendService`, metody dodano do istniejącego `ClientRepository` (zgodnie z wzorcem projektu). Kontroler utworzony jako `front\Controllers\ShopClientController` z DI. + +**DODANE METODY (do istniejącej klasy `ClientRepository`):** +- Simple CRUD: `clientDetails()`, `clientEmail()`, `clientAddresses()`, `addressDetails()`, `addressDelete()`, `addressSave(int $clientId, ?int $addressId, array $data)`, `markAddressAsCurrent()` +- `clientOrders()` — zachowuje zależność od `\front\factory\ShopOrder::order_details()` +- `authenticate(string $email, string $password)` — **BEZ** bypassa 'Legia1916', zwraca `['status' => 'ok'|'error'|'inactive', ...]` +- `createClient()` — zwraca `['id' => ..., 'hash' => ...]` lub null +- `confirmRegistration()` — zwraca email lub null +- `generateNewPassword()` — zwraca `['email' => ..., 'password' => ...]` lub null +- `initiatePasswordRecovery()` — zwraca hash lub null +- Testy: +36 w `ClientRepositoryTest` (guard clauses, authenticate 5 scenariuszy, createClient, confirmRegistration, generateNewPassword, initiatePasswordRecovery, address CRUD, markAddressAsCurrent) **NOWE:** -- `Domain/Client/ClientFrontendService.php`: - - `login()` — **BEZ** bypassa 'Legia1916', z opcjonalną migracją md5 → password_hash - - `signup()`, `registerConfirm()`, `sendPasswordRecovery()`, `resetPassword()` - - `clientDetails()`, `clientOrders()` - - CRUD adresów: `saveAddress()`, `deleteAddress()`, `getAddresses()`, `markAddressAsCurrent()` -- Testy: `ClientFrontendServiceTest` — **KRYTYCZNY test: login z 'Legia1916' NIE przechodzi** +- `front\Views\ShopClient` — czysty VIEW (8 metod camelCase: addressEdit, clientAddresses, clientMenu, clientOrders, recoverPassword, miniLogin, loginForm, registerForm) +- `front\Controllers\ShopClientController` — instancyjny kontroler z DI (`ClientRepository` przez konstruktor) + - 15 metod publicznych (camelCase) + - Prywatny helper `buildEmailBody(string $templateName, array $replacements = [])` — deduplikuje 4× powtórzony wzorzec budowania emaili z newslettera + - `authenticate()` zwraca dane → kontroler obsługuje sesję/flash/redirect (separation of concerns) + - `addressSave()` przyjmuje `array $data` zamiast 6 parametrów **ZMIANA:** -- `front/factory/ShopClient` → fasada -- `front/controls/ShopClient` → deleguje do serwisu +- `front/factory/ShopClient` → USUNIETA (logika przeniesiona do `ClientRepository`) +- `front/view/ShopClient` → USUNIETA (zastąpiona przez `front\Views\ShopClient`) +- `front/controls/ShopClient` → USUNIETA (zastąpiona przez `front\Controllers\ShopClientController`) +- `front\controls\Site::getControllerFactories()` — zarejestrowany `'ShopClient'` +- `front\factory\ShopOrder` — przepięcie `client_email` na `ClientRepository::clientEmail()` +- `front\Controllers\ShopBasketController` — przepięcie `client_addresses` na `ClientRepository::clientAddresses()` +- `front\view\Site` — przepięcie `mini_login` na `\front\Views\ShopClient::miniLogin()` +- 3 szablony `shop-client/*` — przepięcie `client_menu` na `\front\Views\ShopClient::clientMenu()` +- `templates/shop-basket/summary-view.php` — przepięcie `login_form` na `\front\Views\ShopClient::loginForm()` + +**SECURITY FIX:** Hardcoded password bypass `'Legia1916'` usunięty — `authenticate()` sprawdza wyłącznie md5(register_date + password) + +**Testy:** 537 OK, 1648 asercji --- @@ -539,11 +586,11 @@ front\factory\ShopPromotion::promotion_type_XX() → shop\Product::is_product_on | Etap | Zakres | Priorytet | Nowe klasy Domain | Testy | |------|--------|-----------|-------------------|-------| | Settings + Languages | Fundamenty | FUNDAMENT | 2 serwisy | 2 | -| Category Frontend | Kategorie | WYSOKI | 1 serwis | 1 | +| ~~Category Frontend~~ | ~~Kategorie~~ | ZREALIZOWANY | — | — | | ~~Banners/Menu/Pages/Articles/Layouts~~ | ~~Treści~~ | ZREALIZOWANY | — | — | | Promotion Engine | Promocje | KRYTYCZNY | 1 serwis | 1 | | Product Frontend | Produkty | KRYTYCZNY | 1 serwis | 1 | -| Client/Auth (security fix) | Klienci | KRYTYCZNY | 1 serwis | 1 | +| ~~Client/Auth (security fix)~~ | ~~Klienci~~ | ZREALIZOWANY | — | — | | Transport/Payment/Coupon | Dostawa/Płatności | WYSOKI | 3 serwisy | 3 | | Basket Service | Koszyk | WYSOKI | 1 serwis | 1 | | Product Instance + Cache | Produkt cache | ŚREDNI | 1 loader | 1 | diff --git a/docs/PROJECT_STRUCTURE.md b/docs/PROJECT_STRUCTURE.md index 3a03fb0..7f79c5c 100644 --- a/docs/PROJECT_STRUCTURE.md +++ b/docs/PROJECT_STRUCTURE.md @@ -108,8 +108,8 @@ shopPRO/ │ │ ├── Helpers/ # Helpers (ex class.S.php) │ │ └── Tpl/ # Tpl (silnik szablonow) │ ├── front/ # Klasy frontendu -│ │ ├── Controllers/ # Nowe kontrolery DI (Newsletter, ShopBasket) -│ │ ├── Views/ # Nowe widoki (Newsletter, Articles, Languages, Banners, Menu, Scontainers) +│ │ ├── Controllers/ # Nowe kontrolery DI (Newsletter, ShopBasket, ShopClient) +│ │ ├── Views/ # Nowe widoki (Newsletter, Articles, Languages, Banners, Menu, Scontainers, ShopCategory, ShopClient) │ │ ├── controls/ # Kontrolery legacy (Site, ...) │ │ ├── view/ # Widoki legacy (Site, ...) │ │ └── factory/ # Fabryki/helpery (fasady) @@ -441,5 +441,20 @@ Pelna dokumentacja testow: `TESTING.md` - FIX: `libraries/thumb.php` — require przepiety na `Shared/Image/ImageManipulator.php`, poprawiony short open tag. - FIX: `Tpl::render()` branch 3 — sprawdzal `../templates_user/` ale ladowal `../templates/`. +## Aktualizacja 2026-02-17 (ver. 0.289) - ShopCategory + ShopClient frontend migration +- **ShopCategory (frontend)** — migracja factory + view na Domain + Views + - NOWE METODY w `CategoryRepository`: `getCategorySort()`, `categoryName()`, `categoryUrl()`, `frontCategoryDetails()`, `categoriesTree()`, `blogCategoryProducts()`, `categoryProductsCount()`, `productsId()`, `paginatedCategoryProducts()` — z Redis cache, language fallback SQL, stale zamiast magic numbers + - NOWY: `front\Views\ShopCategory` — czysty VIEW (`categoryDescription()`, `categoryView()`, `categories()`) + - USUNIETA: `front\factory\class.ShopCategory.php` — logika przeniesiona do `CategoryRepository` + - USUNIETA: `front\view\class.ShopCategory.php` — zastapiona przez `front\Views\ShopCategory` +- **ShopClient (frontend)** — migracja factory + view + controls na Domain + Views + Controllers + - NOWE METODY w `ClientRepository`: `clientDetails()`, `clientEmail()`, `clientAddresses()`, `addressDetails()`, `addressDelete()`, `addressSave()`, `markAddressAsCurrent()`, `clientOrders()`, `authenticate()`, `createClient()`, `confirmRegistration()`, `generateNewPassword()`, `initiatePasswordRecovery()` + - NOWY: `front\Views\ShopClient` — czysty VIEW (8 metod camelCase) + - NOWY: `front\Controllers\ShopClientController` — instancyjny kontroler z DI (15 metod + `buildEmailBody()` helper) + - USUNIETA: `front\factory\class.ShopClient.php`, `front\view\class.ShopClient.php`, `front\controls\class.ShopClient.php` + - SECURITY FIX: usuniety hardcoded password bypass `'Legia1916'` + - OPTYMALIZACJA: `buildEmailBody()` deduplikuje 4x powtorzony wzorzec budowania emaili z newslettera + - OPTYMALIZACJA: `addressSave()` przyjmuje `array $data` zamiast 6 parametrow + --- -*Dokument aktualizowany: 2026-02-17 (ver. 0.286)* +*Dokument aktualizowany: 2026-02-17 (ver. 0.289)* diff --git a/docs/TESTING.md b/docs/TESTING.md index b8de6d0..309d60b 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -36,7 +36,15 @@ Alternatywnie (Git Bash): Ostatnio zweryfikowano: 2026-02-17 ```text -OK (484 tests, 1528 assertions) +OK (537 tests, 1648 assertions) +``` + +Aktualizacja po migracji ShopCategory + ShopClient frontend (2026-02-17, ver. 0.289): +```text +Pelny suite: OK (537 tests, 1648 assertions) +Nowe testy: CategoryRepositoryTest (+17: getCategorySort, categoryName, categoryUrl, frontCategoryDetails, categoriesTree, blogCategoryProducts, categoryProductsCount, productsId, paginatedCategoryProducts) +Nowe testy: ClientRepositoryTest (+36: clientDetails, clientEmail, clientAddresses, addressDetails, addressDelete, addressSave, markAddressAsCurrent, authenticate 5 scenariuszy, createClient, confirmRegistration, generateNewPassword, initiatePasswordRecovery, clientOrders) +Zaktualizowane: tests/stubs/Helpers.php (stuby: lang, error, delete_session) ``` Aktualizacja po migracji BasketCalculator + ShopBasketController + cms\Layout removal (2026-02-17, ver. 0.288): diff --git a/docs/UPDATE_INSTRUCTIONS.md b/docs/UPDATE_INSTRUCTIONS.md index 41e1e53..a61ddab 100644 --- a/docs/UPDATE_INSTRUCTIONS.md +++ b/docs/UPDATE_INSTRUCTIONS.md @@ -18,16 +18,16 @@ Aktualizacje znajdują się w folderze `updates/0.XX/` gdzie XX oznacza dziesią ## Procedura tworzenia nowej aktualizacji -## Status biezacej aktualizacji (ver. 0.288) +## Status biezacej aktualizacji (ver. 0.289) -- Wersja udostepniona: `0.288` (data: 2026-02-17). +- Wersja udostepniona: `0.289` (data: 2026-02-17). - Pliki publikacyjne: - - `updates/0.20/ver_0.288.zip`, `ver_0.288_files.txt` + - `updates/0.20/ver_0.289.zip`, `ver_0.289_files.txt` - Pliki metadanych aktualizacji: - - `updates/changelog.php` (dodany wpis `ver. 0.288`) - - `updates/versions.php` (`$current_ver = 288`) + - `updates/changelog.php` (dodany wpis `ver. 0.289`) + - `updates/versions.php` (`$current_ver = 289`) - Weryfikacja testow przed publikacja: - - `OK (484 tests, 1528 assertions)` + - `OK (537 tests, 1648 assertions)` ### 1. Określ numer wersji Sprawdź ostatnią wersję w `updates/` i zwiększ o 1. diff --git a/index.php b/index.php index a3b8b23..5281b74 100644 --- a/index.php +++ b/index.php @@ -221,7 +221,7 @@ if ( $settings[ 'piksel' ] ) "track", "ViewCategory", { content_category: "kategoria", - content_name: "' . htmlspecialchars( str_replace( '"', '', \front\factory\ShopCategory::category_name( $category[ 'id' ] ) ) ) . '", + content_name: "' . htmlspecialchars( str_replace( '"', '', ( new \Domain\Category\CategoryRepository( $GLOBALS['mdb'] ) )->categoryName( (int)$category['id'], $lang_id ) ) ) . '", content_ids: ["' . implode( ',', \shop\Category::get_category_products_id( $category['id'] ) ) . '"], content_type: "product" });'; diff --git a/templates/menu/pages.php b/templates/menu/pages.php index 1311385..b13b9b2 100644 --- a/templates/menu/pages.php +++ b/templates/menu/pages.php @@ -10,7 +10,7 @@ if ( is_array( $this -> pages ) ) { if ( $page['page_type'] == 3 ) { $page['language']['link'] ? $url = $page['language']['link'] : $url = '#'; } else if ( $page['page_type'] == 5 ) { - $page['category_id'] ? $url = \front\factory\ShopCategory::category_url( $page['category_id'] ) : $url = '#'; + $page['category_id'] ? $url = ( new \Domain\Category\CategoryRepository( $GLOBALS['mdb'] ) )->categoryUrl( (int)$page['category_id'], $GLOBALS['lang_id'] ) : $url = '#'; } else { $page['language']['seo_link'] ? $url = '/' . $page['language']['seo_link'] : $url = '/s-' . $page['id'] . '-' . \Shared\Helpers\Helpers::seo( $page['language']['title'] ); } diff --git a/templates/shop-basket/summary-view.php b/templates/shop-basket/summary-view.php index 5fbd284..9b80483 100644 --- a/templates/shop-basket/summary-view.php +++ b/templates/shop-basket/summary-view.php @@ -133,7 +133,7 @@ client ):?>
true ] ); ?> diff --git a/templates/shop-category/categories.php b/templates/shop-category/categories.php index 9a31e2d..ed73d98 100644 --- a/templates/shop-category/categories.php +++ b/templates/shop-category/categories.php @@ -28,7 +28,7 @@ if ( is_array( $this -> categories ) ): current_category and $this -> level != 0 ):?> - current_category, $this -> level + 1 );?> + current_category, $this -> level + 1 );?> diff --git a/templates/shop-client/address-edit.php b/templates/shop-client/address-edit.php index f3a46d0..81c3ed7 100644 --- a/templates/shop-client/address-edit.php +++ b/templates/shop-client/address-edit.php @@ -1,5 +1,5 @@
- 'addresses' ] );?>
diff --git a/templates/shop-client/client-addresses.php b/templates/shop-client/client-addresses.php index bb56d72..2d6d69a 100644 --- a/templates/shop-client/client-addresses.php +++ b/templates/shop-client/client-addresses.php @@ -1,5 +1,5 @@
- 'addresses' ] );?>
diff --git a/templates/shop-client/client-orders.php b/templates/shop-client/client-orders.php index 21a269a..4f3a1c7 100644 --- a/templates/shop-client/client-orders.php +++ b/templates/shop-client/client-orders.php @@ -1,6 +1,6 @@
- 'orders' ] );?>
diff --git a/tests/Unit/Domain/Category/CategoryRepositoryTest.php b/tests/Unit/Domain/Category/CategoryRepositoryTest.php index 2817d5d..5a27a1d 100644 --- a/tests/Unit/Domain/Category/CategoryRepositoryTest.php +++ b/tests/Unit/Domain/Category/CategoryRepositoryTest.php @@ -205,4 +205,262 @@ class CategoryRepositoryTest extends TestCase $this->assertSame('Kategoria testowa', $repository->categoryTitle(10)); } + + // ===== Frontend methods tests ===== + + public function testGetCategorySortReturnsZeroForInvalidId(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->never())->method('get'); + + $repository = new CategoryRepository($mockDb); + + $this->assertSame(0, $repository->getCategorySort(0)); + $this->assertSame(0, $repository->getCategorySort(-1)); + } + + public function testGetCategorySortReturnsSortType(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->method('get') + ->with('pp_shop_categories', 'sort_type', ['id' => 5]) + ->willReturn('3'); + + $repository = new CategoryRepository($mockDb); + + $this->assertSame(3, $repository->getCategorySort(5)); + } + + public function testCategoryNameReturnsEmptyForInvalidInput(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->never())->method('get'); + + $repository = new CategoryRepository($mockDb); + + $this->assertSame('', $repository->categoryName(0, 'pl')); + $this->assertSame('', $repository->categoryName(5, '')); + } + + public function testCategoryNameReturnsTitle(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->method('get') + ->with('pp_shop_categories_langs', 'title', [ + 'AND' => [ + 'category_id' => 10, + 'lang_id' => 'pl', + ], + ]) + ->willReturn('Elektronika'); + + $repository = new CategoryRepository($mockDb); + + $this->assertSame('Elektronika', $repository->categoryName(10, 'pl')); + } + + public function testCategoryNameReturnsEmptyWhenNotFound(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->method('get')->willReturn(null); + + $repository = new CategoryRepository($mockDb); + + $this->assertSame('', $repository->categoryName(10, 'pl')); + } + + public function testFrontCategoryDetailsReturnsEmptyForInvalidId(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->never())->method('get'); + + $repository = new CategoryRepository($mockDb); + + $this->assertSame([], $repository->frontCategoryDetails(0, 'pl')); + $this->assertSame([], $repository->frontCategoryDetails(-1, 'en')); + } + + public function testFrontCategoryDetailsReturnsCategoryWithLanguage(): void + { + $mockDb = $this->createMock(\medoo::class); + + $callIndex = 0; + $mockDb->method('get') + ->willReturnCallback(function ($table) use (&$callIndex) { + $callIndex++; + if ($table === 'pp_shop_categories') { + return [ + 'id' => 7, + 'status' => 1, + 'sort_type' => 2, + 'parent_id' => null, + ]; + } + if ($table === 'pp_shop_categories_langs') { + return [ + 'category_id' => 7, + 'lang_id' => 'pl', + 'title' => 'Odzież', + 'seo_link' => 'odziez', + ]; + } + return null; + }); + + $repository = new CategoryRepository($mockDb); + $result = $repository->frontCategoryDetails(7, 'pl'); + + $this->assertSame(7, $result['id']); + $this->assertSame('Odzież', $result['language']['title']); + $this->assertSame('odziez', $result['language']['seo_link']); + } + + public function testFrontCategoryDetailsReturnsEmptyWhenCategoryNotFound(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->method('get')->willReturn(null); + + $repository = new CategoryRepository($mockDb); + + $this->assertSame([], $repository->frontCategoryDetails(999, 'pl')); + } + + public function testCategoriesTreeReturnsEmptyWhenNoCategories(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->method('select')->willReturn([]); + + $repository = new CategoryRepository($mockDb); + + $this->assertSame([], $repository->categoriesTree('pl')); + } + + public function testCategoryProductsCountReturnsZeroForInvalidInput(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->never())->method('query'); + + $repository = new CategoryRepository($mockDb); + + $this->assertSame(0, $repository->categoryProductsCount(0, 'pl')); + $this->assertSame(0, $repository->categoryProductsCount(5, '')); + } + + public function testCategoryProductsCountReturnsCount(): void + { + $mockDb = $this->createMock(\medoo::class); + + $stmt = $this->createMock(\PDOStatement::class); + $stmt->method('fetchAll')->willReturn([[15]]); + + $mockDb->method('query')->willReturn($stmt); + + $repository = new CategoryRepository($mockDb); + + $this->assertSame(15, $repository->categoryProductsCount(3, 'pl')); + } + + public function testProductsIdReturnsEmptyForInvalidInput(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->never())->method('query'); + + $repository = new CategoryRepository($mockDb); + + $this->assertSame([], $repository->productsId(0, 0, 'pl', 12, 0)); + $this->assertSame([], $repository->productsId(5, 0, '', 12, 0)); + } + + public function testProductsIdReturnsProductIds(): void + { + $mockDb = $this->createMock(\medoo::class); + + $stmt = $this->createMock(\PDOStatement::class); + $stmt->method('fetchAll')->willReturn([ + ['id' => 101], + ['id' => 102], + ['id' => 103], + ]); + + $mockDb->method('query')->willReturn($stmt); + + $repository = new CategoryRepository($mockDb); + $result = $repository->productsId(5, 1, 'pl', 12, 0); + + $this->assertSame([101, 102, 103], $result); + } + + public function testBlogCategoryProductsReturnsEmptyForInvalidInput(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->never())->method('query'); + + $repository = new CategoryRepository($mockDb); + + $this->assertSame([], $repository->blogCategoryProducts(0, 'pl', 5)); + $this->assertSame([], $repository->blogCategoryProducts(5, '', 5)); + $this->assertSame([], $repository->blogCategoryProducts(5, 'pl', 0)); + } + + public function testBlogCategoryProductsReturnsIds(): void + { + $mockDb = $this->createMock(\medoo::class); + + $stmt = $this->createMock(\PDOStatement::class); + $stmt->method('fetchAll')->willReturn([ + ['id' => 201], + ['id' => 202], + ]); + + $mockDb->method('query')->willReturn($stmt); + + $repository = new CategoryRepository($mockDb); + $result = $repository->blogCategoryProducts(3, 'pl', 5); + + $this->assertSame([201, 202], $result); + } + + public function testPaginatedCategoryProductsReturnsEmptyWhenNoProducts(): void + { + $mockDb = $this->createMock(\medoo::class); + + $stmt = $this->createMock(\PDOStatement::class); + $stmt->method('fetchAll')->willReturn([[0]]); + + $mockDb->method('query')->willReturn($stmt); + + $repository = new CategoryRepository($mockDb); + $result = $repository->paginatedCategoryProducts(5, 0, 'pl', 1); + + $this->assertSame([], $result['products']); + $this->assertSame(0, $result['ls']); + } + + public function testPaginatedCategoryProductsClampsPage(): void + { + $mockDb = $this->createMock(\medoo::class); + + $countStmt = $this->createMock(\PDOStatement::class); + $countStmt->method('fetchAll')->willReturn([[25]]); + + $productsStmt = $this->createMock(\PDOStatement::class); + $productsStmt->method('fetchAll')->willReturn([ + ['id' => 301], + ['id' => 302], + ]); + + $callIndex = 0; + $mockDb->method('query') + ->willReturnCallback(function () use (&$callIndex, $countStmt, $productsStmt) { + $callIndex++; + return $callIndex === 1 ? $countStmt : $productsStmt; + }); + + $repository = new CategoryRepository($mockDb); + + // 25 products / 12 per page = 3 pages; page 99 should clamp to 3 + $result = $repository->paginatedCategoryProducts(5, 0, 'pl', 99); + $this->assertSame(3, $result['ls']); + $this->assertSame([301, 302], $result['products']); + } } diff --git a/tests/Unit/Domain/Client/ClientRepositoryTest.php b/tests/Unit/Domain/Client/ClientRepositoryTest.php index f5981ad..e84a1e4 100644 --- a/tests/Unit/Domain/Client/ClientRepositoryTest.php +++ b/tests/Unit/Domain/Client/ClientRepositoryTest.php @@ -131,4 +131,505 @@ class ClientRepositoryTest extends TestCase $this->assertSame(4, $totals['total_orders']); $this->assertSame(456.78, $totals['total_spent']); } + + // ===== Frontend methods ===== + + public function testClientDetailsReturnsNullForInvalidId(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->never())->method('get'); + + $repo = new ClientRepository($mockDb); + $this->assertNull($repo->clientDetails(0)); + $this->assertNull($repo->clientDetails(-1)); + } + + public function testClientDetailsReturnsRowOnSuccess(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('get') + ->with('pp_shop_clients', '*', ['id' => 5]) + ->willReturn(['id' => 5, 'email' => 'jan@example.com']); + + $repo = new ClientRepository($mockDb); + $result = $repo->clientDetails(5); + $this->assertSame('jan@example.com', $result['email']); + } + + public function testClientDetailsReturnsNullWhenNotFound(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('get') + ->willReturn(false); + + $repo = new ClientRepository($mockDb); + $this->assertNull($repo->clientDetails(999)); + } + + public function testClientEmailReturnsNullForInvalidId(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->never())->method('get'); + + $repo = new ClientRepository($mockDb); + $this->assertNull($repo->clientEmail(0)); + } + + public function testClientEmailReturnsStringOnSuccess(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('get') + ->with('pp_shop_clients', 'email', ['id' => 3]) + ->willReturn('test@example.com'); + + $repo = new ClientRepository($mockDb); + $this->assertSame('test@example.com', $repo->clientEmail(3)); + } + + public function testClientEmailReturnsNullWhenNotFound(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('get') + ->willReturn(false); + + $repo = new ClientRepository($mockDb); + $this->assertNull($repo->clientEmail(999)); + } + + public function testClientAddressesReturnsEmptyForInvalidId(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->never())->method('select'); + + $repo = new ClientRepository($mockDb); + $this->assertSame([], $repo->clientAddresses(0)); + } + + public function testClientAddressesReturnsRows(): void + { + $rows = [ + ['id' => 1, 'client_id' => 5, 'city' => 'Warszawa'], + ['id' => 2, 'client_id' => 5, 'city' => 'Kraków'], + ]; + + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('select') + ->with('pp_shop_clients_addresses', '*', ['client_id' => 5]) + ->willReturn($rows); + + $repo = new ClientRepository($mockDb); + $this->assertCount(2, $repo->clientAddresses(5)); + } + + public function testClientAddressesHandlesFalseFromDb(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('select') + ->willReturn(false); + + $repo = new ClientRepository($mockDb); + $this->assertSame([], $repo->clientAddresses(1)); + } + + public function testAddressDetailsReturnsNullForInvalidId(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->never())->method('get'); + + $repo = new ClientRepository($mockDb); + $this->assertNull($repo->addressDetails(0)); + } + + public function testAddressDetailsReturnsRow(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('get') + ->with('pp_shop_clients_addresses', '*', ['id' => 7]) + ->willReturn(['id' => 7, 'city' => 'Gdańsk']); + + $repo = new ClientRepository($mockDb); + $result = $repo->addressDetails(7); + $this->assertSame('Gdańsk', $result['city']); + } + + public function testAddressDeleteReturnsFalseForInvalidId(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->never())->method('delete'); + + $repo = new ClientRepository($mockDb); + $this->assertFalse($repo->addressDelete(0)); + } + + public function testAddressDeleteReturnsTrueOnSuccess(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('delete') + ->with('pp_shop_clients_addresses', ['id' => 3]) + ->willReturn(1); + + $repo = new ClientRepository($mockDb); + $this->assertTrue($repo->addressDelete(3)); + } + + public function testAddressSaveReturnsFalseForInvalidClientId(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->never())->method('insert'); + $mockDb->expects($this->never())->method('update'); + + $repo = new ClientRepository($mockDb); + $this->assertFalse($repo->addressSave(0, null, ['name' => 'Jan'])); + } + + public function testAddressSaveInsertsNewAddress(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('insert') + ->with( + 'pp_shop_clients_addresses', + $this->callback(function ($row) { + return $row['client_id'] === 5 + && $row['name'] === 'Jan' + && $row['city'] === 'Warszawa'; + }) + ) + ->willReturn(1); + + $repo = new ClientRepository($mockDb); + $result = $repo->addressSave(5, null, [ + 'name' => 'Jan', + 'surname' => 'Kowalski', + 'street' => 'Marszałkowska 1', + 'postal_code' => '00-001', + 'city' => 'Warszawa', + 'phone' => '123456789', + ]); + $this->assertTrue($result); + } + + public function testAddressSaveUpdatesExistingAddress(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('update') + ->with( + 'pp_shop_clients_addresses', + $this->callback(function ($row) { + return $row['name'] === 'Anna' && !isset($row['client_id']); + }), + ['AND' => ['client_id' => 5, 'id' => 10]] + ) + ->willReturn(1); + + $repo = new ClientRepository($mockDb); + $result = $repo->addressSave(5, 10, [ + 'name' => 'Anna', + 'surname' => 'Nowak', + 'street' => 'Piłsudskiego 2', + 'postal_code' => '30-001', + 'city' => 'Kraków', + 'phone' => '987654321', + ]); + $this->assertTrue($result); + } + + public function testMarkAddressAsCurrentReturnsFalseForInvalidIds(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->never())->method('update'); + + $repo = new ClientRepository($mockDb); + $this->assertFalse($repo->markAddressAsCurrent(0, 1)); + $this->assertFalse($repo->markAddressAsCurrent(1, 0)); + } + + public function testMarkAddressAsCurrentResetsAndSets(): void + { + $calls = []; + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->exactly(2)) + ->method('update') + ->willReturnCallback(function ($table, $data, $where) use (&$calls) { + $calls[] = ['data' => $data, 'where' => $where]; + return 1; + }); + + $repo = new ClientRepository($mockDb); + $this->assertTrue($repo->markAddressAsCurrent(5, 3)); + + $this->assertSame(['current' => 0], $calls[0]['data']); + $this->assertSame(['client_id' => 5], $calls[0]['where']); + $this->assertSame(['current' => 1], $calls[1]['data']); + $this->assertSame(['AND' => ['client_id' => 5, 'id' => 3]], $calls[1]['where']); + } + + public function testAuthenticateReturnsErrorOnEmptyInput(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->never())->method('get'); + + $repo = new ClientRepository($mockDb); + + $result = $repo->authenticate('', 'pass'); + $this->assertSame('error', $result['status']); + + $result = $repo->authenticate('jan@example.com', ''); + $this->assertSame('error', $result['status']); + } + + public function testAuthenticateReturnsErrorWhenClientNotFound(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('get') + ->willReturn(false); + + $repo = new ClientRepository($mockDb); + $result = $repo->authenticate('nobody@example.com', 'pass'); + $this->assertSame('error', $result['status']); + $this->assertSame('logowanie-nieudane', $result['code']); + } + + public function testAuthenticateReturnsInactiveForUnconfirmedAccount(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('get') + ->willReturn([ + 'id' => 1, + 'password' => md5('2026-01-01 00:00:00' . 'test123'), + 'register_date' => '2026-01-01 00:00:00', + 'hash' => 'abc123', + 'status' => 0, + ]); + + $repo = new ClientRepository($mockDb); + $result = $repo->authenticate('jan@example.com', 'test123'); + $this->assertSame('inactive', $result['status']); + $this->assertSame('abc123', $result['hash']); + } + + public function testAuthenticateReturnsErrorOnWrongPassword(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('get') + ->willReturn([ + 'id' => 1, + 'password' => md5('2026-01-01 00:00:00' . 'correct'), + 'register_date' => '2026-01-01 00:00:00', + 'hash' => 'abc', + 'status' => 1, + ]); + + $repo = new ClientRepository($mockDb); + $result = $repo->authenticate('jan@example.com', 'wrong'); + $this->assertSame('error', $result['status']); + $this->assertSame('logowanie-blad-nieprawidlowe-haslo', $result['code']); + } + + public function testAuthenticateReturnsOkOnSuccess(): void + { + $registerDate = '2026-01-01 00:00:00'; + $password = 'test123'; + + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->exactly(2)) + ->method('get') + ->willReturnOnConsecutiveCalls( + [ + 'id' => 5, + 'password' => md5($registerDate . $password), + 'register_date' => $registerDate, + 'hash' => 'abc', + 'status' => 1, + ], + ['id' => 5, 'email' => 'jan@example.com', 'name' => 'Jan'] + ); + + $repo = new ClientRepository($mockDb); + $result = $repo->authenticate('jan@example.com', $password); + $this->assertSame('ok', $result['status']); + $this->assertSame(5, $result['client']['id']); + } + + public function testCreateClientReturnsNullOnEmptyInput(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->never())->method('count'); + + $repo = new ClientRepository($mockDb); + $this->assertNull($repo->createClient('', 'pass', false)); + $this->assertNull($repo->createClient('jan@example.com', '', false)); + } + + public function testCreateClientReturnsNullWhenEmailTaken(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('count') + ->with('pp_shop_clients', ['email' => 'jan@example.com']) + ->willReturn(1); + + $repo = new ClientRepository($mockDb); + $this->assertNull($repo->createClient('jan@example.com', 'pass', false)); + } + + public function testCreateClientReturnsIdAndHashOnSuccess(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once())->method('count')->willReturn(0); + $mockDb->expects($this->once()) + ->method('insert') + ->with( + 'pp_shop_clients', + $this->callback(function ($row) { + return $row['email'] === 'jan@example.com' + && $row['agremment_marketing'] === 1 + && !empty($row['hash']) + && !empty($row['password']); + }) + ) + ->willReturn(1); + $mockDb->expects($this->once())->method('id')->willReturn(42); + + $repo = new ClientRepository($mockDb); + $result = $repo->createClient('jan@example.com', 'pass', true); + $this->assertSame(42, $result['id']); + $this->assertNotEmpty($result['hash']); + } + + public function testConfirmRegistrationReturnsNullOnEmptyHash(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->never())->method('get'); + + $repo = new ClientRepository($mockDb); + $this->assertNull($repo->confirmRegistration('')); + } + + public function testConfirmRegistrationReturnsNullWhenNotFound(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('get') + ->willReturn(false); + + $repo = new ClientRepository($mockDb); + $this->assertNull($repo->confirmRegistration('nonexistent')); + } + + public function testConfirmRegistrationActivatesAndReturnsEmail(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->exactly(2)) + ->method('get') + ->willReturnOnConsecutiveCalls(10, 'jan@example.com'); + $mockDb->expects($this->once()) + ->method('update') + ->with('pp_shop_clients', ['status' => 1], ['id' => 10]); + + $repo = new ClientRepository($mockDb); + $this->assertSame('jan@example.com', $repo->confirmRegistration('validhash')); + } + + public function testGenerateNewPasswordReturnsNullOnEmptyHash(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->never())->method('get'); + + $repo = new ClientRepository($mockDb); + $this->assertNull($repo->generateNewPassword('')); + } + + public function testGenerateNewPasswordReturnsNullWhenNotFound(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('get') + ->willReturn(false); + + $repo = new ClientRepository($mockDb); + $this->assertNull($repo->generateNewPassword('badhash')); + } + + public function testGenerateNewPasswordReturnsEmailAndPassword(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('get') + ->willReturn([ + 'id' => 5, + 'email' => 'jan@example.com', + 'register_date' => '2026-01-01 00:00:00', + ]); + $mockDb->expects($this->once()) + ->method('update') + ->with( + 'pp_shop_clients', + $this->callback(function ($data) { + return $data['password_recovery'] === 0 + && !empty($data['password']); + }), + ['id' => 5] + ); + + $repo = new ClientRepository($mockDb); + $result = $repo->generateNewPassword('validhash'); + $this->assertSame('jan@example.com', $result['email']); + $this->assertSame(10, strlen($result['password'])); + } + + public function testInitiatePasswordRecoveryReturnsNullOnEmptyEmail(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->never())->method('get'); + + $repo = new ClientRepository($mockDb); + $this->assertNull($repo->initiatePasswordRecovery('')); + } + + public function testInitiatePasswordRecoveryReturnsNullWhenNotFound(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('get') + ->willReturn(false); + + $repo = new ClientRepository($mockDb); + $this->assertNull($repo->initiatePasswordRecovery('nobody@example.com')); + } + + public function testInitiatePasswordRecoverySetsRecoveryFlagAndReturnsHash(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('get') + ->willReturn('abc123hash'); + $mockDb->expects($this->once()) + ->method('update') + ->with('pp_shop_clients', ['password_recovery' => 1], ['email' => 'jan@example.com']); + + $repo = new ClientRepository($mockDb); + $this->assertSame('abc123hash', $repo->initiatePasswordRecovery('jan@example.com')); + } + + public function testClientOrdersReturnsEmptyForInvalidId(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->never())->method('select'); + + $repo = new ClientRepository($mockDb); + $this->assertSame([], $repo->clientOrders(0)); + } } diff --git a/tests/stubs/Helpers.php b/tests/stubs/Helpers.php index b2894de..ed0232a 100644 --- a/tests/stubs/Helpers.php +++ b/tests/stubs/Helpers.php @@ -6,6 +6,9 @@ class Helpers public static function seo($str) { return $str; } public static function delete_dir($path) {} public static function alert($msg) {} + public static function error($msg) {} + public static function lang($key) { return $key; } + public static function delete_session($key) { unset($_SESSION[$key]); } public static function htacces() {} public static function delete_cache() {} public static function get($key) { return null; } diff --git a/updates/0.20/ver_0.289.zip b/updates/0.20/ver_0.289.zip new file mode 100644 index 0000000..b542c79 Binary files /dev/null and b/updates/0.20/ver_0.289.zip differ diff --git a/updates/0.20/ver_0.289_files.txt b/updates/0.20/ver_0.289_files.txt new file mode 100644 index 0000000..7deccfa --- /dev/null +++ b/updates/0.20/ver_0.289_files.txt @@ -0,0 +1,5 @@ +F: ../autoload/front/factory/class.ShopCategory.php +F: ../autoload/front/view/class.ShopCategory.php +F: ../autoload/front/factory/class.ShopClient.php +F: ../autoload/front/view/class.ShopClient.php +F: ../autoload/front/controls/class.ShopClient.php diff --git a/updates/changelog.php b/updates/changelog.php index e5ae1b5..0f016ba 100644 --- a/updates/changelog.php +++ b/updates/changelog.php @@ -1,3 +1,8 @@ +ver. 0.289 - 17.02.2026
+- UPDATE - migracja front\factory\ShopCategory + front\view\ShopCategory do Domain\Category\CategoryRepository + front\Views\ShopCategory +- UPDATE - migracja front\factory\ShopClient + front\view\ShopClient + front\controls\ShopClient do Domain\Client\ClientRepository + front\Views\ShopClient + front\Controllers\ShopClientController +- FIX - usuniety hardcoded password bypass 'Legia1916' w logowaniu klienta +
ver. 0.288 - 17.02.2026
- UPDATE - migracja front\factory\ShopBasket do Domain\Basket\BasketCalculator (4 metody statyczne) - UPDATE - migracja front\controls\ShopBasket do front\Controllers\ShopBasketController (camelCase, instancyjny) diff --git a/updates/versions.php b/updates/versions.php index a3c5d39..911755b 100644 --- a/updates/versions.php +++ b/updates/versions.php @@ -1,5 +1,5 @@