From a8175c0944c9f3d10e1d541276a4f232ba001f59 Mon Sep 17 00:00:00 2001 From: Jacek Pyziak Date: Fri, 27 Feb 2026 22:06:33 +0100 Subject: [PATCH] feat: eliminate htaccess.conf, move all URL routes to pp_routes (v0.329-0.330) - Add category_id, page_id, article_id, type columns to pp_routes (migration 0.329) - Move routing block in index.php before checkUrlParams() with Redis cache - Routes for categories, pages, articles now stored in pp_routes instead of .htaccess - Delete category/page/article routes on entity delete in respective repositories - Eliminate libraries/htaccess.conf: generate .htaccess content entirely from PHP - Move 32 static system routes (koszyk, logowanie, newsletter, AJAX modules, etc.) plus dynamic language/producer routes to pp_routes with type='system' - Invalidate pp_routes Redis cache on every htacces() regeneration Co-Authored-By: Claude Sonnet 4.6 --- autoload/Domain/Article/ArticleRepository.php | 4 + .../Domain/Category/CategoryRepository.php | 1 + autoload/Domain/Pages/PagesRepository.php | 6 +- autoload/Shared/Helpers/Helpers.php | 400 +++++++---- docs/DATABASE_STRUCTURE.md | 25 + docs/TODO.md | 3 +- .../2026-02-27-htaccess-conf-elimination.md | 658 ++++++++++++++++++ .../2026-02-27-htaccess-to-routes-design.md | 121 ++++ index.php | 76 +- libraries/htaccess.conf | 84 --- migrations/0.329.sql | 5 + .../Domain/Article/ArticleRepositoryTest.php | 7 +- .../Category/CategoryRepositoryTest.php | 11 +- 13 files changed, 1139 insertions(+), 262 deletions(-) create mode 100644 docs/plans/2026-02-27-htaccess-conf-elimination.md create mode 100644 docs/plans/2026-02-27-htaccess-to-routes-design.md delete mode 100644 libraries/htaccess.conf create mode 100644 migrations/0.329.sql diff --git a/autoload/Domain/Article/ArticleRepository.php b/autoload/Domain/Article/ArticleRepository.php index 2c47203..d432f95 100644 --- a/autoload/Domain/Article/ArticleRepository.php +++ b/autoload/Domain/Article/ArticleRepository.php @@ -360,6 +360,9 @@ class ArticleRepository public function archive(int $articleId): bool { $result = $this->db->update('pp_articles', ['status' => -1], ['id' => $articleId]); + if ($result) { + $this->db->delete('pp_routes', ['article_id' => $articleId]); + } return (bool)$result; } @@ -381,6 +384,7 @@ class ArticleRepository $this->db->delete('pp_articles_langs', ['article_id' => $articleId]); $this->db->delete('pp_articles_images', ['article_id' => $articleId]); $this->db->delete('pp_articles_files', ['article_id' => $articleId]); + $this->db->delete('pp_routes', ['article_id' => $articleId]); $this->db->delete('pp_articles', ['id' => $articleId]); \Shared\Helpers\Helpers::delete_dir('../upload/article_images/article_' . $articleId . '/'); diff --git a/autoload/Domain/Category/CategoryRepository.php b/autoload/Domain/Category/CategoryRepository.php index e3bf16e..484ae41 100644 --- a/autoload/Domain/Category/CategoryRepository.php +++ b/autoload/Domain/Category/CategoryRepository.php @@ -174,6 +174,7 @@ class CategoryRepository $deleted = (bool)$this->db->delete('pp_shop_categories', ['id' => $id]); if ($deleted) { + $this->db->delete('pp_routes', ['category_id' => $id]); $this->refreshCategoryArtifacts(); } diff --git a/autoload/Domain/Pages/PagesRepository.php b/autoload/Domain/Pages/PagesRepository.php index 8548d7c..3b86f8c 100644 --- a/autoload/Domain/Pages/PagesRepository.php +++ b/autoload/Domain/Pages/PagesRepository.php @@ -134,7 +134,11 @@ class PagesRepository return false; } - return (bool)$this->db->delete('pp_pages', ['id' => $pageId]); + $deleted = (bool)$this->db->delete('pp_pages', ['id' => $pageId]); + if ($deleted) { + $this->db->delete('pp_routes', ['page_id' => $pageId]); + } + return $deleted; } /** diff --git a/autoload/Shared/Helpers/Helpers.php b/autoload/Shared/Helpers/Helpers.php index 7011e95..0d42ad6 100644 --- a/autoload/Shared/Helpers/Helpers.php +++ b/autoload/Shared/Helpers/Helpers.php @@ -425,42 +425,206 @@ class Helpers $site_map .= '1' . PHP_EOL; $site_map .= '' . PHP_EOL; - $htaccess_data = file_get_contents( $dir . 'libraries/htaccess.conf' ); - $htaccess_data = str_replace( '{PAGE}', $url, $htaccess_data ); + // + // SYSTEM ROUTES — delete all and reinsert + // + $mdb->delete( 'pp_routes', [ 'type' => 'system' ] ); - $results = $mdb -> select( 'pp_langs', [ 'id' ], [ 'status' => 1, 'ORDER' => [ 'o' => 'ASC' ] ] ); + // Static system routes (hardcoded, never change) + $systemRoutes = [ + // Wyszukiwarka + [ 'pattern' => '^wyszukiwarka/([^/]+)/([0-9]+)$', 'destination' => 'index.php?module=search&action=search_results&query=$1&bs=$2' ], + [ 'pattern' => '^wyszukiwarka/([^/]+)$', 'destination' => 'index.php?module=search&action=search_results&query=$1&bs=1' ], + // Zamowienia + [ 'pattern' => '^zamowienie/([a-zA-Z0-9-]+)$', 'destination' => 'index.php?module=shop_order&action=order_details&order_hash=$1' ], + [ 'pattern' => '^potwierdzenie-platnosci/([a-zA-Z0-9-]+)$', 'destination' => 'index.php?module=shop_order&action=payment_confirmation&order_hash=$1' ], + // Platnosci + [ 'pattern' => '^tpay-status$', 'destination' => 'index.php?module=shop_order&action=payment_status_tpay' ], + [ 'pattern' => '^platnosc-status$', 'destination' => 'index.php?module=shop_order&action=payment_status_hotpay' ], + [ 'pattern' => '^przelewy24-status$', 'destination' => 'index.php?module=shop_order&action=payment_status_przelewy24pl' ], + // Koszyk + [ 'pattern' => '^koszyk$', 'destination' => 'index.php?module=shop_basket&action=main_view' ], + [ 'pattern' => '^koszyk-podsumowanie$', 'destination' => 'index.php?module=shop_basket&action=summary_view' ], + [ 'pattern' => '^zloz-zamowienie$', 'destination' => 'index.php?module=shop_basket&action=basket_save' ], + // Klient + [ 'pattern' => '^rejestracja$', 'destination' => 'index.php?module=shop_client&action=register_form' ], + [ 'pattern' => '^logowanie$', 'destination' => 'index.php?module=shop_client&action=login_form' ], + [ 'pattern' => '^wylogowanie$', 'destination' => 'index.php?module=shop_client&action=logout' ], + [ 'pattern' => '^odzyskiwanie-hasla$', 'destination' => 'index.php?module=shop_client&action=recover_password' ], + [ 'pattern' => '^panel-klienta/zamowienia$', 'destination' => 'index.php?module=shop_client&action=client_orders' ], + [ 'pattern' => '^panel-klienta/adresy$', 'destination' => 'index.php?module=shop_client&action=client_addresses' ], + [ 'pattern' => '^panel-klienta/nowy-adres$', 'destination' => 'index.php?module=shop_client&action=address_edit' ], + [ 'pattern' => '^panel-klienta/edytuj-adres/([0-9]+)$', 'destination' => 'index.php?module=shop_client&action=address_edit&id=$1' ], + [ 'pattern' => '^panel-klienta/usun-adres/([0-9]+)$', 'destination' => 'index.php?module=shop_client&action=address_delete&id=$1' ], + // Newsletter + [ 'pattern' => '^newsletter/signin$', 'destination' => 'index.php?module=newsletter&action=signin' ], + [ 'pattern' => '^newsletter/confirm/hash=(.+)$', 'destination' => 'index.php?module=newsletter&action=confirm&hash=$1' ], + [ 'pattern' => '^newsletter/unsubscribe/hash=(.+)$', 'destination' => 'index.php?module=newsletter&action=unsubscribe&hash=$1' ], + // Moduły AJAX (shopBasket, shopClient, shopProduct, shopCoupon, search) + [ 'pattern' => '^shopBasket/([^/]+)/(.+)$', 'destination' => 'index.php?module=shopBasket&action=$1&$2' ], + [ 'pattern' => '^shopBasket/([^/]+)$', 'destination' => 'index.php?module=shopBasket&action=$1' ], + [ 'pattern' => '^shopClient/([^/]+)/(.+)$', 'destination' => 'index.php?module=shopClient&action=$1&$2' ], + [ 'pattern' => '^shopClient/([^/]+)$', 'destination' => 'index.php?module=shopClient&action=$1' ], + [ 'pattern' => '^shopProduct/([^/]+)/(.+)$', 'destination' => 'index.php?module=shopProduct&action=$1&$2' ], + [ 'pattern' => '^shopProduct/([^/]+)$', 'destination' => 'index.php?module=shopProduct&action=$1' ], + [ 'pattern' => '^shopCoupon/([^/]+)/(.+)$', 'destination' => 'index.php?module=shopCoupon&action=$1&$2' ], + [ 'pattern' => '^shopCoupon/([^/]+)$', 'destination' => 'index.php?module=shopCoupon&action=$1' ], + [ 'pattern' => '^search/([^/]+)/(.+)$', 'destination' => 'index.php?module=search&action=$1&$2' ], + [ 'pattern' => '^search/([^/]+)$', 'destination' => 'index.php?module=search&action=$1' ], + ]; + + foreach ( $systemRoutes as $route ) + { + $mdb->insert( 'pp_routes', [ + 'type' => 'system', + 'lang_id' => 0, + 'pattern' => $route['pattern'], + 'destination' => $route['destination'], + ] ); + } + + // Dynamic system routes — languages + $results = $mdb->select( 'pp_langs', [ 'id' ], [ 'status' => 1, 'ORDER' => [ 'o' => 'ASC' ] ] ); if ( is_array( $results ) ) foreach ( $results as $row ) { - $htaccess_data .= PHP_EOL . 'RewriteRule ^' . $row['id'] . '/$ index.php?a=change_language&id=' . $row['id'] . ' [L]'; + $mdb->insert( 'pp_routes', [ + 'type' => 'system', + 'lang_id' => 0, + 'pattern' => '^' . $row['id'] . '$', + 'destination' => 'index.php?a=change_language&id=' . $row['id'], + ] ); } - // - // INNE - // - $htaccess_data .= PHP_EOL; - $htaccess_data .= 'RewriteRule ^newsletter/signin/$ index.php?module=newsletter&action=signin [L]' . PHP_EOL; - $htaccess_data .= 'RewriteRule ^newsletter/confirm/hash=(.*)$ index.php?module=newsletter&action=confirm&hash=$1 [L]' . PHP_EOL; - $htaccess_data .= 'RewriteRule ^newsletter/unsubscribe/hash=(.*)$ index.php?module=newsletter&action=unsubscribe&hash=$1 [L]' . PHP_EOL; - - // - // PRODUCENCI - // + // Dynamic system routes — producenci $categoryDefaultLayoutId = ( new \Domain\Layouts\LayoutsRepository( $mdb ) )->categoryDefaultLayoutId(); - $htaccess_data .= 'RewriteRule ^producenci$ index.php?module=shop_producer&action=list&layout_id=' . $categoryDefaultLayoutId . '&%{QUERY_STRING} [L]' . PHP_EOL; - $rows = $mdb -> select( 'pp_shop_producer', '*', [ 'status' => 1 ] ); + $mdb->insert( 'pp_routes', [ + 'type' => 'system', + 'lang_id' => 0, + 'pattern' => '^producenci$', + 'destination' => 'index.php?module=shop_producer&action=list&layout_id=' . $categoryDefaultLayoutId, + ] ); + + $rows = $mdb->select( 'pp_shop_producer', '*', [ 'status' => 1 ] ); if ( self::is_array_fix( $rows ) ) foreach ( $rows as $row ) { - $htaccess_data .= 'RewriteRule ^producent/' . self::seo( $row['name'] ) . '$ index.php?module=shop_producer&action=products&producer_id=' . $row['id'] . '&layout_id=' . $categoryDefaultLayoutId . '&%{QUERY_STRING} [L]' . PHP_EOL; - $htaccess_data .= 'RewriteRule ^producent/' . self::seo( $row['name'] ) . '/([0-9]+)$ index.php?module=shop_producer&action=products&producer_id=' . $row['id'] . '&layout_id=' . $categoryDefaultLayoutId . '&bs=$1&%{QUERY_STRING} [L]' . PHP_EOL; + $mdb->insert( 'pp_routes', [ + 'type' => 'system', + 'lang_id' => 0, + 'pattern' => '^producent/' . self::seo( $row['name'] ) . '$', + 'destination' => 'index.php?module=shop_producer&action=products&producer_id=' . $row['id'] . '&layout_id=' . $categoryDefaultLayoutId, + ] ); + $mdb->insert( 'pp_routes', [ + 'type' => 'system', + 'lang_id' => 0, + 'pattern' => '^producent/' . self::seo( $row['name'] ) . '/([0-9]+)$', + 'destination' => 'index.php?module=shop_producer&action=products&producer_id=' . $row['id'] . '&layout_id=' . $categoryDefaultLayoutId . '&bs=$1', + ] ); } - $results = $mdb -> select( 'pp_langs', [ 'id', 'start' ], [ 'status' => 1, 'ORDER' => [ 'o' => 'ASC' ] ] ); + // + // HTACCESS — generuj z PHP (bez szablonu htaccess.conf) + // + $htaccess_data = 'RewriteEngine On' . PHP_EOL; + $htaccess_data .= 'RewriteBase /' . PHP_EOL; + $htaccess_data .= 'Options +FollowSymlinks' . PHP_EOL; + $htaccess_data .= 'Options -Indexes' . PHP_EOL; + $htaccess_data .= PHP_EOL; + $htaccess_data .= '# Przekierowanie z www na bez www i z http na https w jednym kroku' . PHP_EOL; + $htaccess_data .= 'RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]' . PHP_EOL; + $htaccess_data .= 'RewriteRule ^ https://%1%{REQUEST_URI} [R=301,L]' . PHP_EOL; + $htaccess_data .= PHP_EOL; + $htaccess_data .= '# Przekierowanie z http na https, jesli nie zawiera www' . PHP_EOL; + $htaccess_data .= 'RewriteCond %{HTTPS} off' . PHP_EOL; + $htaccess_data .= 'RewriteCond %{REQUEST_URI} !^/(tpay-status|platnosc-status|przelewy24-status)$ [NC]' . PHP_EOL; + $htaccess_data .= 'RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]' . PHP_EOL; + $htaccess_data .= PHP_EOL; + $htaccess_data .= '# Usuwanie koncowego slasha dla niekatalogów' . PHP_EOL; + $htaccess_data .= 'RewriteCond %{REQUEST_FILENAME} !-d' . PHP_EOL; + $htaccess_data .= 'RewriteCond %{REQUEST_URI} !^/admin/.*$ [NC]' . PHP_EOL; + $htaccess_data .= 'RewriteCond %{REQUEST_URI} (.+)/$' . PHP_EOL; + $htaccess_data .= 'RewriteRule ^ %1 [R=301,L]' . PHP_EOL; + $htaccess_data .= PHP_EOL; + $htaccess_data .= 'RewriteCond %{REQUEST_URI} !^(.*)/libraries/(.*) [NC]' . PHP_EOL; + $htaccess_data .= 'RewriteCond %{REQUEST_URI} !^(.*)/layout/(.*) [NC]' . PHP_EOL; + $htaccess_data .= 'RewriteRule ^admin/([^/]*)/([^/]*)/(.*)$ admin/index.php?module=$1&action=$2&$3 [L]' . PHP_EOL; + $htaccess_data .= PHP_EOL; + $htaccess_data .= 'RewriteRule ^admin/$ admin/index.php [L]' . PHP_EOL; + $htaccess_data .= PHP_EOL; + $htaccess_data .= 'RewriteRule ^thumb/([0-9]*)/([0-9]*)/(.*)$ /libraries/thumb.php?img=$3&w=$1&h=$2 [L]' . PHP_EOL; + $htaccess_data .= PHP_EOL; + $htaccess_data .= 'RewriteCond %{THE_REQUEST} ^[A-Z]{3,9}\ /index.php' . PHP_EOL; + $htaccess_data .= 'RewriteRule ^ /%1 [R=301,L]' . PHP_EOL; + + /* cache block */ + if ( $settings['htaccess_cache'] ) + { + $htaccess_data .= '' . PHP_EOL + . 'AddOutputFilterByType DEFLATE text/html text/plain text/xml application/xml application/xhtml+xml text/css text/javascript application/javascript application/x-javascript' . PHP_EOL + . '' . PHP_EOL + . '' . PHP_EOL + . 'Header set Access-Control-Allow-Origin "*"' . PHP_EOL + . '' . PHP_EOL + . '' . PHP_EOL + . 'ExpiresActive on' . PHP_EOL + . 'ExpiresDefault "access plus 1 month"' . PHP_EOL + . 'ExpiresByType text/css "access plus 1 year"' . PHP_EOL + . 'ExpiresByType application/json "access plus 0 seconds"' . PHP_EOL + . 'ExpiresByType application/xml "access plus 0 seconds"' . PHP_EOL + . 'ExpiresByType text/xml "access plus 0 seconds"' . PHP_EOL + . 'ExpiresByType image/x-icon "access plus 1 week"' . PHP_EOL + . 'ExpiresByType text/x-component "access plus 1 month"' . PHP_EOL + . 'ExpiresByType text/html "access plus 0 seconds"' . PHP_EOL + . 'ExpiresByType application/javascript "access plus 1 year"' . PHP_EOL + . 'ExpiresByType application/x-web-app-manifest+json "access plus 0 seconds"' . PHP_EOL + . 'ExpiresByType text/cache-manifest "access plus 0 seconds"' . PHP_EOL + . 'ExpiresByType audio/ogg "access plus 1 month"' . PHP_EOL + . 'ExpiresByType image/gif "access plus 1 month"' . PHP_EOL + . 'ExpiresByType image/jpeg "access plus 1 month"' . PHP_EOL + . 'ExpiresByType image/png "access plus 1 month"' . PHP_EOL + . 'ExpiresByType video/mp4 "access plus 1 month"' . PHP_EOL + . 'ExpiresByType video/ogg "access plus 1 month"' . PHP_EOL + . 'ExpiresByType video/webm "access plus 1 month"' . PHP_EOL + . 'ExpiresByType application/atom+xml "access plus 1 hour"' . PHP_EOL + . 'ExpiresByType application/rss+xml "access plus 1 hour"' . PHP_EOL + . 'ExpiresByType application/font-woff "access plus 1 month"' . PHP_EOL + . 'ExpiresByType application/vnd.ms-fontobject "access plus 1 month"' . PHP_EOL + . 'ExpiresByType application/x-font-ttf "access plus 1 month"' . PHP_EOL + . 'ExpiresByType font/opentype "access plus 1 month"' . PHP_EOL + . 'ExpiresByType image/svg+xml "access plus 1 month"' . PHP_EOL + . '' . PHP_EOL; + } + else + { + $htaccess_data .= '' . PHP_EOL + . 'Header set Cache-Control "no-cache, no-store, must-revalidate"' . PHP_EOL + . 'Header set Pragma "no-cache"' . PHP_EOL + . 'Header set Expires 0' . PHP_EOL + . '' . PHP_EOL; + } + + $htaccess_data .= '' . PHP_EOL; + $htaccess_data .= ' Order Deny,Allow' . PHP_EOL; + $htaccess_data .= ' Deny from all' . PHP_EOL; + $htaccess_data .= '' . PHP_EOL; + $htaccess_data .= '' . PHP_EOL; + $htaccess_data .= ' Order Deny,Allow' . PHP_EOL; + $htaccess_data .= ' Deny from all' . PHP_EOL; + $htaccess_data .= '' . PHP_EOL; + $htaccess_data .= '' . PHP_EOL; + $htaccess_data .= ' Order Deny,Allow' . PHP_EOL; + $htaccess_data .= ' Deny from all' . PHP_EOL; + $htaccess_data .= '' . PHP_EOL; + + // + // KATEGORIE — sitemap + pp_routes (bez zmian) + // + $results = $mdb->select( 'pp_langs', [ 'id', 'start' ], [ 'status' => 1, 'ORDER' => [ 'o' => 'ASC' ] ] ); if ( is_array( $results ) ) foreach ( $results as $row ) { !$row['start'] ? $language_link = $row['id'] . '/' : $language_link = ''; - $results2 = $mdb -> select( 'pp_shop_categories_langs', [ '[><]pp_shop_categories' => [ 'category_id' => 'id' ] ], [ 'seo_link', 'title', 'category_id' ], [ 'lang_id' => $row['id'], 'ORDER' => [ 'o' => 'ASC' ] ] ); + $results2 = $mdb->select( 'pp_shop_categories_langs', [ '[><]pp_shop_categories' => [ 'category_id' => 'id' ] ], [ 'seo_link', 'title', 'category_id' ], [ 'lang_id' => $row['id'], 'ORDER' => [ 'o' => 'ASC' ] ] ); if ( is_array( $results2 ) ) foreach ( $results2 as $row2 ) { if ( $row2['title'] ) @@ -475,35 +639,42 @@ class Helpers $site_map .= '1' . PHP_EOL; $site_map .= '' . PHP_EOL; - if ( $row2['seo_link'] ) - { - $htaccess_data .= PHP_EOL . 'RewriteRule ^' . $language_link . self::seo( $row2['seo_link'] ) . '$ index.php?category=' . $row2['category_id'] . '&lang=' . $row['id'] . '&%{QUERY_STRING} [L]'; - $htaccess_data .= PHP_EOL . 'RewriteRule ^' . $language_link . self::seo( $row2['seo_link'] ) . '/([0-9]+)$ index.php?category=' . $row2['category_id'] . '&lang=' . $row['id'] . '&bs=$1&%{QUERY_STRING} [L]'; - $htaccess_data .= PHP_EOL . 'RewriteRule ^' . $language_link . self::seo( $row2['seo_link'] ) . '/1$ ' . $language_link . self::seo( $row2['seo_link'] ) . ' [R=301,L]'; - } - else - { - $htaccess_data .= PHP_EOL . 'RewriteRule ^' . $language_link . 'k-' . $row2['category_id'] . '-' . self::seo( $row2['title'] ) . '$ index.php?category=' . $row2['category_id'] . '&lang=' . $row['id'] . '&%{QUERY_STRING} [L]'; - $htaccess_data .= PHP_EOL . 'RewriteRule ^' . $language_link . 'k-' . $row2['category_id'] . '-' . self::seo( $row2['title'] ) . '/([0-9]+)$ index.php?category=' . $row2['category_id'] . '&lang=' . $row['id'] . '&bs=$1&%{QUERY_STRING} [L]'; - $htaccess_data .= PHP_EOL . 'RewriteRule ^' . $language_link . 'k-' . $row2['category_id'] . '-' . self::seo( $row2['title'] ) . '/1$ ' . $language_link . 'k-' . $row2['category_id'] . '-' . self::seo( $row2['title'] ) . ' [R=301,L]'; - } + $seoSlug = $row2['seo_link'] ? self::seo( $row2['seo_link'] ) : 'k-' . $row2['category_id'] . '-' . self::seo( $row2['title'] ); + + $mdb->delete( 'pp_routes', [ 'AND' => [ 'category_id' => $row2['category_id'], 'lang_id' => $row['id'] ] ] ); + + $mdb->insert( 'pp_routes', [ + 'category_id' => $row2['category_id'], + 'lang_id' => $row['id'], + 'pattern' => '^' . $language_link . $seoSlug . '$', + 'destination' => 'index.php?category=' . $row2['category_id'] . '&lang=' . $row['id'], + ] ); + $mdb->insert( 'pp_routes', [ + 'category_id' => $row2['category_id'], + 'lang_id' => $row['id'], + 'pattern' => '^' . $language_link . $seoSlug . '/([0-9]+)$', + 'destination' => 'index.php?category=' . $row2['category_id'] . '&lang=' . $row['id'] . '&bs=$1', + ] ); } } } - $results = $mdb -> select( 'pp_langs', [ 'id', 'start' ], [ 'status' => 1, 'ORDER' => [ 'o' => 'ASC' ] ] ); + // + // PRODUKTY — sitemap + pp_routes (bez zmian) + // + $results = $mdb->select( 'pp_langs', [ 'id', 'start' ], [ 'status' => 1, 'ORDER' => [ 'o' => 'ASC' ] ] ); if ( is_array( $results ) ) { foreach ( $results as $row ) { !$row['start'] ? $language_link = $row['id'] . '/' : $language_link = ''; - $results2 = $mdb -> select( 'pp_shop_products_langs', [ '[><]pp_shop_products' => [ 'product_id' => 'id' ] ], [ 'seo_link', 'name', 'product_id' ], [ 'lang_id' => $row['id'], 'ORDER' => [ 'name' => 'ASC' ] ] ); + $results2 = $mdb->select( 'pp_shop_products_langs', [ '[><]pp_shop_products' => [ 'product_id' => 'id' ] ], [ 'seo_link', 'name', 'product_id' ], [ 'lang_id' => $row['id'], 'ORDER' => [ 'name' => 'ASC' ] ] ); if ( is_array( $results2 ) ) { foreach ( $results2 as $row2 ) { - $mdb -> delete( 'pp_routes', [ 'AND' => [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'] ] ] ); + $mdb->delete( 'pp_routes', [ 'AND' => [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'] ] ] ); if ( $row2['name'] ) { @@ -519,27 +690,13 @@ class Helpers if ( $row2['seo_link'] ) { - $pattern = '^' . $language_link . self::seo( $row2['seo_link'] ) . '$'; - $destination = 'index.php?product=' . $row2['product_id']; - - $mdb -> insert( 'pp_routes', [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'], 'pattern' => $pattern, 'destination' => $destination ] ); - - $pattern = '^' . $language_link . self::seo( $row2['seo_link'] ) . '/([0-9-]+)$'; - $destination = 'index.php?product=' . $row2['product_id'] . '&permutation_hash=$1'; - - $mdb -> insert( 'pp_routes', [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'], 'pattern' => $pattern, 'destination' => $destination ] ); + $mdb->insert( 'pp_routes', [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'], 'pattern' => '^' . $language_link . self::seo( $row2['seo_link'] ) . '$', 'destination' => 'index.php?product=' . $row2['product_id'] ] ); + $mdb->insert( 'pp_routes', [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'], 'pattern' => '^' . $language_link . self::seo( $row2['seo_link'] ) . '/([0-9-]+)$', 'destination' => 'index.php?product=' . $row2['product_id'] . '&permutation_hash=$1' ] ); } else { - $pattern = '^' . $language_link . 'p-' . $row2['product_id'] . '-' . self::seo( $row2['name'] ) . '$'; - $destination = 'index.php?product=' . $row2['product_id']; - - $mdb -> insert( 'pp_routes', [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'], 'pattern' => $pattern, 'destination' => $destination ] ); - - $pattern = '^' . $language_link . 'p-' . $row2['product_id'] . '-' . self::seo( $row2['name'] ) . '/([0-9-]+)$'; - $destination = 'index.php?product=' . $row2['product_id'] . '&permutation_hash=$1'; - - $mdb -> insert( 'pp_routes', [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'], 'pattern' => $pattern, 'destination' => $destination ] ); + $mdb->insert( 'pp_routes', [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'], 'pattern' => '^' . $language_link . 'p-' . $row2['product_id'] . '-' . self::seo( $row2['name'] ) . '$', 'destination' => 'index.php?product=' . $row2['product_id'] ] ); + $mdb->insert( 'pp_routes', [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'], 'pattern' => '^' . $language_link . 'p-' . $row2['product_id'] . '-' . self::seo( $row2['name'] ) . '/([0-9-]+)$', 'destination' => 'index.php?product=' . $row2['product_id'] . '&permutation_hash=$1' ] ); } } } @@ -547,13 +704,16 @@ class Helpers } } - $results = $mdb -> select( 'pp_langs', [ 'id', 'start' ], [ 'status' => 1, 'ORDER' => [ 'o' => 'ASC' ] ] ); + // + // STRONY + ARTYKULY — sitemap + pp_routes (bez zmian) + // + $results = $mdb->select( 'pp_langs', [ 'id', 'start' ], [ 'status' => 1, 'ORDER' => [ 'o' => 'ASC' ] ] ); if ( is_array( $results ) ) foreach ( $results as $row ) { ( !$row['start'] and count( $results ) > 1 ) ? $language_link = $row['id'] . '/' : $language_link = ''; - $results2 = $mdb -> select( 'pp_pages_langs', [ '[><]pp_pages' => [ 'page_id' => 'id' ] ], [ 'seo_link', 'title', 'page_id', 'noindex', 'start', 'link', 'page_type' ], [ 'lang_id' => $row['id'], 'ORDER' => [ 'start' => 'DESC', 'o' => 'ASC' ] ] ); + $results2 = $mdb->select( 'pp_pages_langs', [ '[><]pp_pages' => [ 'page_id' => 'id' ] ], [ 'seo_link', 'title', 'page_id', 'noindex', 'start', 'link', 'page_type' ], [ 'lang_id' => $row['id'], 'ORDER' => [ 'start' => 'DESC', 'o' => 'ASC' ] ] ); if ( is_array( $results2 ) ) foreach ( $results2 as $row2 ) { @@ -590,44 +750,39 @@ class Helpers { $htaccess_data .= PHP_EOL . 'RewriteCond %{REQUEST_URI} ^/s-' . $row2['page_id'] . '-' . self::seo( $row2['title'] ) . '$'; $htaccess_data .= PHP_EOL . 'RewriteRule ^(.*)$ http://www.' . $url . '/' . $language_link . ' [R=permanent,L]'; - $htaccess_data .= PHP_EOL . 'RewriteCond %{REQUEST_URI} ^/s-' . $row2['page_id'] . '-' . self::seo( $row2['title'] ) . '-1$'; $htaccess_data .= PHP_EOL . 'RewriteRule ^(.*)$ http://www.' . $url . '/' . $language_link . ' [R=permanent,L]'; } - $htaccess_data .= PHP_EOL . 'RewriteRule ^$ index.php?a=page&id=' . $row2['page_id'] . '&lang=' . $row['id'] . ' [L]'; } - if ( $row2['seo_link'] ) - { - $htaccess_data .= PHP_EOL . 'RewriteRule ^' . $language_link . self::seo( $row2['seo_link'] ) . '$ index.php?a=page&id=' . $row2['page_id'] . '&lang=' . $row['id'] . '&%{QUERY_STRING} [L]'; - $htaccess_data .= PHP_EOL . 'RewriteRule ^' . $language_link . self::seo( $row2['seo_link'] ) . '/([0-9]+)$ index.php?a=page&id=' . $row2['page_id'] . '&lang=' . $row['id'] . '&bs=$1&%{QUERY_STRING} [L]'; - $htaccess_data .= PHP_EOL . 'RewriteRule ^' . $language_link . self::seo( $row2['seo_link'] ) . '/1$ ' . $language_link . self::seo( $row2['seo_link'] ) . ' [R=301,L]'; - } - else - { - $htaccess_data .= PHP_EOL . 'RewriteRule ^' . $language_link . 's-' . $row2['page_id'] . '-' . self::seo( $row2['title'] ) . '$ index.php?a=page&id=' . $row2['page_id'] . '&lang=' . $row['id'] . '&%{QUERY_STRING} [L]'; - $htaccess_data .= PHP_EOL . 'RewriteRule ^' . $language_link . 's-' . $row2['page_id'] . '-' . self::seo( $row2['title'] ) . '/([0-9]+)$ index.php?a=page&id=' . $row2['page_id'] . '&lang=' . $row['id'] . '&bs=$1&%{QUERY_STRING} [L]'; - $htaccess_data .= PHP_EOL . 'RewriteRule ^' . $language_link . 's-' . $row2['page_id'] . '-' . self::seo( $row2['title'] ) . '/1$ ' . $language_link . 's-' . $row2['page_id'] . '-' . self::seo( $row2['title'] ) . ' [R=301,L]'; - } + $seoSlug = $row2['seo_link'] ? self::seo( $row2['seo_link'] ) : 's-' . $row2['page_id'] . '-' . self::seo( $row2['title'] ); + $langPrefix = $row2['start'] ? '' : $language_link; + + $mdb->delete( 'pp_routes', [ 'AND' => [ 'page_id' => $row2['page_id'], 'lang_id' => $row['id'] ] ] ); + + $mdb->insert( 'pp_routes', [ + 'page_id' => $row2['page_id'], + 'lang_id' => $row['id'], + 'pattern' => '^' . $langPrefix . $seoSlug . '$', + 'destination' => 'index.php?a=page&id=' . $row2['page_id'] . '&lang=' . $row['id'], + ] ); + $mdb->insert( 'pp_routes', [ + 'page_id' => $row2['page_id'], + 'lang_id' => $row['id'], + 'pattern' => '^' . $langPrefix . $seoSlug . '/([0-9]+)$', + 'destination' => 'index.php?a=page&id=' . $row2['page_id'] . '&lang=' . $row['id'] . '&bs=$1', + ] ); } } - $results2 = $mdb -> select( 'pp_articles_langs', [ '[><]pp_articles' => [ 'article_id' => 'id' ] ], [ 'seo_link', 'title', 'article_id', 'noindex', 'copy_from' ], [ 'AND' => [ 'status' => 1, 'lang_id' => $row['id'], 'block_direct_access' => 0 ] ] ); + $results2 = $mdb->select( 'pp_articles_langs', [ '[><]pp_articles' => [ 'article_id' => 'id' ] ], [ 'seo_link', 'title', 'article_id', 'noindex', 'copy_from' ], [ 'AND' => [ 'status' => 1, 'lang_id' => $row['id'], 'block_direct_access' => 0 ] ] ); if ( is_array( $results2 ) ) foreach ( $results2 as $row2 ) { if ( $row2['copy_from'] != null ) { - $results_tmp = $mdb -> get( 'pp_articles_langs', [ - 'seo_link', - 'title' - ], [ - 'AND' => [ - 'article_id' => $row2['article_id'], - 'lang_id' => $row2['copy_from'] - ] - ] ); + $results_tmp = $mdb->get( 'pp_articles_langs', [ 'seo_link', 'title' ], [ 'AND' => [ 'article_id' => $row2['article_id'], 'lang_id' => $row2['copy_from'] ] ] ); $row2['seo_link'] = $results_tmp['seo_link']; $row2['title'] = $results_tmp['title']; } @@ -650,81 +805,52 @@ class Helpers $robots .= 'Disallow: /' . $row2['seo_link'] . PHP_EOL; } + $mdb->delete( 'pp_routes', [ 'AND' => [ 'article_id' => $row2['article_id'], 'lang_id' => $row['id'] ] ] ); + if ( $row2['seo_link'] ) - $htaccess_data .= PHP_EOL . 'RewriteRule ^' . $language_link . self::seo( $row2['seo_link'] ) . '$ index.php?article=' . $row2['article_id'] . '&lang=' . $row['id'] . '&%{QUERY_STRING} [L]'; + { + $mdb->insert( 'pp_routes', [ + 'article_id' => $row2['article_id'], + 'lang_id' => $row['id'], + 'pattern' => '^' . $language_link . self::seo( $row2['seo_link'] ) . '$', + 'destination' => 'index.php?article=' . $row2['article_id'] . '&lang=' . $row['id'], + ] ); + } else if ( $row2['title'] != null ) - $htaccess_data .= PHP_EOL . 'RewriteRule ^' . $language_link . 'a-' . $row2['article_id'] . '-' . self::seo( $row2['title'] ) . '$ index.php?article=' . $row2['article_id'] . '&lang=' . $row['id'] . '&%{QUERY_STRING} [L]'; + { + $mdb->insert( 'pp_routes', [ + 'article_id' => $row2['article_id'], + 'lang_id' => $row['id'], + 'pattern' => '^' . $language_link . 'a-' . $row2['article_id'] . '-' . self::seo( $row2['title'] ) . '$', + 'destination' => 'index.php?article=' . $row2['article_id'] . '&lang=' . $row['id'], + ] ); + } } } - $results = $mdb -> get( 'pp_settings', 'value', [ 'param' => 'htaccess' ] ); + // Invalidacja cache tras + try { + ( new \Shared\Cache\CacheHandler() )->delete( 'pp_routes:all' ); + } catch ( \Exception $e ) { + // Redis niedostepny — ignorujemy + } + + $results = $mdb->get( 'pp_settings', 'value', [ 'param' => 'htaccess' ] ); if ( $results ) $htaccess_data .= PHP_EOL . $results; - $results = $mdb -> get( 'pp_settings', 'value', [ 'param' => 'robots' ] ); + $results = $mdb->get( 'pp_settings', 'value', [ 'param' => 'robots' ] ); if ( $results ) - $robots .= PHP_EOL . $results; + $robots .= PHP_EOL . $results; $site_map .= ''; - /* cache */ - if ( $settings['htaccess_cache'] ) - { - $htaccess_data = str_replace( '{HTACCESS_CACHE}', - '' . PHP_EOL - . 'AddOutputFilterByType DEFLATE text/html text/plain text/xml application/xml application/xhtml+xml text/css text/javascript application/javascript application/x-javascript' . PHP_EOL - . '' . PHP_EOL - . '' . PHP_EOL - . 'Header set Access-Control-Allow-Origin "*"' . PHP_EOL - . '' . PHP_EOL - . '' . PHP_EOL - . 'ExpiresActive on' . PHP_EOL - . 'ExpiresDefault "access plus 1 month"' . PHP_EOL - . 'ExpiresByType text/css "access plus 1 year"' . PHP_EOL - . 'ExpiresByType application/json "access plus 0 seconds"' . PHP_EOL - . 'ExpiresByType application/xml "access plus 0 seconds"' . PHP_EOL - . 'ExpiresByType text/xml "access plus 0 seconds"' . PHP_EOL - . 'ExpiresByType image/x-icon "access plus 1 week"' . PHP_EOL - . 'ExpiresByType text/x-component "access plus 1 month"' . PHP_EOL - . 'ExpiresByType text/html "access plus 0 seconds"' . PHP_EOL - . 'ExpiresByType application/javascript "access plus 1 year"' . PHP_EOL - . 'ExpiresByType application/x-web-app-manifest+json "access plus 0 seconds"' . PHP_EOL - . 'ExpiresByType text/cache-manifest "access plus 0 seconds"' . PHP_EOL - . 'ExpiresByType audio/ogg "access plus 1 month"' . PHP_EOL - . 'ExpiresByType image/gif "access plus 1 month"' . PHP_EOL - . 'ExpiresByType image/jpeg "access plus 1 month"' . PHP_EOL - . 'ExpiresByType image/png "access plus 1 month"' . PHP_EOL - . 'ExpiresByType video/mp4 "access plus 1 month"' . PHP_EOL - . 'ExpiresByType video/ogg "access plus 1 month"' . PHP_EOL - . 'ExpiresByType video/webm "access plus 1 month"' . PHP_EOL - . 'ExpiresByType application/atom+xml "access plus 1 hour"' . PHP_EOL - . 'ExpiresByType application/rss+xml "access plus 1 hour"' . PHP_EOL - . 'ExpiresByType application/font-woff "access plus 1 month"' . PHP_EOL - . 'ExpiresByType application/vnd.ms-fontobject "access plus 1 month"' . PHP_EOL - . 'ExpiresByType application/x-font-ttf "access plus 1 month"' . PHP_EOL - . 'ExpiresByType font/opentype "access plus 1 month"' . PHP_EOL - . 'ExpiresByType image/svg+xml "access plus 1 month"' . PHP_EOL - . '' - , $htaccess_data ); - } - else - { - $htaccess_data = str_replace( '{HTACCESS_CACHE}', - '' . PHP_EOL - . 'Header set Cache-Control "no-cache, no-store, must-revalidate"' . PHP_EOL - . 'Header set Pragma "no-cache"' . PHP_EOL - . 'Header set Expires 0' . PHP_EOL - . '', - $htaccess_data ); - } - $htaccess_data .= PHP_EOL; $htaccess_data .= 'RewriteCond %{REQUEST_FILENAME} !-f' . PHP_EOL; $htaccess_data .= 'RewriteCond %{REQUEST_FILENAME} !-d' . PHP_EOL; $htaccess_data .= 'RewriteRule ^ index.php [L]'; // Niektore hostingi blokuja zmiane wersji PHP przez .htaccess. - // Automatycznie komentujemy niedozwolone dyrektywy, aby generowany plik byl kompatybilny. $htaccess_data = preg_replace( '/^(\\s*)(AddHandler|SetHandler|ForceType)\\b/im', '$1# $2', $htaccess_data ); $fp = fopen( $dir . '.htaccess', 'w' ); diff --git a/docs/DATABASE_STRUCTURE.md b/docs/DATABASE_STRUCTURE.md index b061a2b..efafebd 100644 --- a/docs/DATABASE_STRUCTURE.md +++ b/docs/DATABASE_STRUCTURE.md @@ -700,3 +700,28 @@ Harmonogram cyklicznych zadań cron. **Używane w:** `Domain\CronJob\CronJobRepository`, `Domain\CronJob\CronJobProcessor` **Dodano w wersji 0.324.** + +## pp_routes +Tabela tras URL — mapowanie wzorców URL (regex) na parametry GET. Zastępuje reguły `RewriteRule` w `.htaccess` dla wszystkich URL-i aplikacji: produktów, kategorii, stron, artykułów oraz systemowych (koszyk, logowanie, newsletter, itp.). + +| Kolumna | Opis | +|---------|------| +| id | Klucz główny (AUTO_INCREMENT) | +| product_id | ID produktu (INT NULL) — wypełnione dla tras produktów | +| category_id | ID kategorii (INT NULL) — wypełnione dla tras kategorii | +| page_id | ID strony (INT NULL) — wypełnione dla tras stron | +| article_id | ID artykułu (INT NULL) — wypełnione dla tras artykułów | +| type | Typ trasy: NULL = encja (produkt/kategoria/strona/artykuł), `'system'` = trasa systemowa (koszyk, logowanie, newsletter, AJAX moduły, itp.) | +| lang_id | ID języka (0 dla tras systemowych niezwiązanych z językiem) | +| pattern | Wyrażenie regularne dopasowywane do REQUEST_URI | +| destination | Docelowy query string, np. `index.php?category=5&lang=1` | + +**Mechanizm:** `index.php` ładuje wszystkie trasy (z cache Redis `pp_routes:all`) przed `checkUrlParams()`, dopasowuje `pattern` do ścieżki żądania i ustawia `$_GET` z `destination`. Obsługuje grupy przechwytujące (np. `$1` dla paginacji). + +**Trasy systemowe:** Przy każdym wywołaniu `Helpers::htacces()` wszystkie rekordy z `type='system'` są usuwane i wstawiane na nowo (32 statycznych + dynamiczne trasy językowe i producentów). Zarządzane automatycznie — nie edytować ręcznie. + +**Cache:** Redis klucz `pp_routes:all`, TTL 86400s. Invalidowany automatycznie przy każdym wywołaniu `Helpers::htacces()`. + +**Używane w:** `index.php`, `Shared\Helpers\Helpers::htacces()`, `Domain\Product\ProductRepository`, `Domain\Category\CategoryRepository`, `Domain\Pages\PagesRepository`, `Domain\Article\ArticleRepository` + +**Dodano w wersji 0.329. Kolumna `type` i trasy systemowe dodane w wersji 0.330.** diff --git a/docs/TODO.md b/docs/TODO.md index 6af1edf..66eef60 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -1,3 +1,4 @@ 1. Dodać przycisk kopiowania przy atrybutach produktu w zamówieniu 2. Poprawić htaccess, żeby w nim nie było w ogóle adresów strona wszystko z bazy. -3. Dodać uwierzytelnienie dwuskładnikowe za pomocą aplikacji. \ No newline at end of file +3. Dodać uwierzytelnienie dwuskładnikowe za pomocą aplikacji. +4. Dodać zarządzanie uprawnieniami na poziomie urzytkownika, na razie uprawnienia do poszczególnych modułów. \ No newline at end of file diff --git a/docs/plans/2026-02-27-htaccess-conf-elimination.md b/docs/plans/2026-02-27-htaccess-conf-elimination.md new file mode 100644 index 0000000..2024753 --- /dev/null +++ b/docs/plans/2026-02-27-htaccess-conf-elimination.md @@ -0,0 +1,658 @@ +# htaccess.conf Elimination — Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Eliminate `libraries/htaccess.conf` as a template file and move all remaining hardcoded URL routes into `pp_routes`, leaving only true Apache-level directives in the generated `.htaccess`. + +**Architecture:** `Helpers::htacces()` generates the full `.htaccess` content from PHP strings instead of loading a template. All URL→PHP mappings (static system routes + dynamic per language/producer) are inserted into `pp_routes` with `type='system'`, deleted and reinserted on every `htacces()` call. Apache-level rules (HTTPS redirect, admin routing, thumb.php) stay in `.htaccess` only. + +**Tech Stack:** PHP 7.4, Medoo ORM (`$mdb`), Redis (CacheHandler), PHPUnit 9.6 + +--- + +## Context + +### Current `Helpers::htacces()` structure (before this plan) +1. Loads `libraries/htaccess.conf` template (contains many hardcoded URL routes) +2. Appends language switch rules to `$htaccess_data` +3. Appends newsletter and producer rules to `$htaccess_data` +4. Inserts category/product/page/article routes into `pp_routes` (done in v0.329) +5. Replaces `{HTACCESS_CACHE}` placeholder +6. Appends catch-all, writes files + +### What stays in `.htaccess` after this plan +- `RewriteEngine On`, `RewriteBase /`, `Options` +- www→https redirect +- http→https redirect (with tpay/przelewy24/hotpay exclusion) +- Trailing slash removal (excluding `/admin/`) +- Admin routing: `^admin/([^/]*)/([^/]*)/(.*)$` +- `^admin/$` +- Thumbnail: `^thumb/([0-9]*)/([0-9]*)/(.*)$` → `/libraries/thumb.php` (different PHP file, cannot use pp_routes) +- `THE_REQUEST` index.php redirect +- Cache headers block (gzip/expires or no-cache based on `$settings['htaccess_cache']`) +- File protection: ``, ``, `` +- Start page 301 redirects (generated dynamically in pages loop) +- Custom htaccess from `pp_settings` (param=htaccess) +- Catch-all: `RewriteRule ^ index.php [L]` + +### New `type` column in `pp_routes` +- `NULL` = entity route (product/category/page/article) +- `'system'` = system route (all routes in this plan) +- On every `htacces()` call: `DELETE WHERE type='system'`, then reinsert all + +--- + +## Task 1: Update SQL migration — add `type` column + +**Files:** +- Modify: `migrations/0.329.sql` + +**Step 1: Add `type` column to the migration** + +Open `migrations/0.329.sql` (currently has 4 lines). Append the `type` column: + +```sql +ALTER TABLE pp_routes + ADD COLUMN category_id INT NULL AFTER product_id, + ADD COLUMN page_id INT NULL AFTER category_id, + ADD COLUMN article_id INT NULL AFTER page_id, + ADD COLUMN type VARCHAR(20) NULL AFTER article_id; +``` + +**Step 2: Apply migration on server** + +Run on the production/staging database: +```sql +ALTER TABLE pp_routes ADD COLUMN type VARCHAR(20) NULL AFTER article_id; +``` +(The other 3 columns from 0.329 should already be applied from the previous deployment.) + +**Step 3: No test needed** — pure schema change, verified when routes are inserted in Task 2. + +--- + +## Task 2: Refactor `Helpers::htacces()` — replace template + move all routes to pp_routes + +**Files:** +- Modify: `autoload/Shared/Helpers/Helpers.php` (method `htacces()`, lines ~408–773) + +This is the core task. The entire method is refactored. Here is the complete new body: + +**Step 1: Replace the method body** + +Find the opening of `htacces()` at line ~408. Replace everything from the start of the method body through the end (line ~773) with the code below. + +The key structural changes: +- Remove `file_get_contents(htaccess.conf)` and `str_replace('{PAGE}', ...)` +- Remove `str_replace('{HTACCESS_CACHE}', ...)` — cache block is now inline +- Build `$htaccess_data` directly as PHP string +- Delete all `type='system'` routes, then reinsert static + dynamic ones +- Language switch → `pp_routes` (remove from `$htaccess_data`) +- Newsletter → `pp_routes` (remove from `$htaccess_data`) +- Producenci/producent → `pp_routes` (remove from `$htaccess_data`) + +**New `htacces()` method body** — replace lines 409–773 with: + +```php + { + global $mdb; + + $settings = ( new \Domain\Settings\SettingsRepository( $mdb ) )->allSettings( true ); + + $url = preg_replace( '#^(http(s)?://)?w{3}\.#', '$1', $_SERVER['SERVER_NAME'] ); + + $robots = 'User-agent: *' . PHP_EOL; + $robots .= 'Allow: /' . PHP_EOL; + + $site_map = '' . PHP_EOL; + $site_map .= '' . PHP_EOL; + $site_map .= '' . PHP_EOL; + $site_map .= 'https://' . $url . '' . PHP_EOL; + $site_map .= '' . date( 'Y-m-d' ) . '' . PHP_EOL; + $site_map .= 'daily' . PHP_EOL; + $site_map .= '1' . PHP_EOL; + $site_map .= '' . PHP_EOL; + + // + // SYSTEM ROUTES — delete all and reinsert + // + $mdb->delete( 'pp_routes', [ 'type' => 'system' ] ); + + // Static system routes (hardcoded, never change) + $systemRoutes = [ + // Wyszukiwarka + [ 'pattern' => '^wyszukiwarka/([^/]+)/([0-9]+)$', 'destination' => 'index.php?module=search&action=search_results&query=$1&bs=$2' ], + [ 'pattern' => '^wyszukiwarka/([^/]+)$', 'destination' => 'index.php?module=search&action=search_results&query=$1&bs=1' ], + // Zamowienia + [ 'pattern' => '^zamowienie/([a-zA-Z0-9-]+)$', 'destination' => 'index.php?module=shop_order&action=order_details&order_hash=$1' ], + [ 'pattern' => '^potwierdzenie-platnosci/([a-zA-Z0-9-]+)$', 'destination' => 'index.php?module=shop_order&action=payment_confirmation&order_hash=$1' ], + // Platnosci + [ 'pattern' => '^tpay-status$', 'destination' => 'index.php?module=shop_order&action=payment_status_tpay' ], + [ 'pattern' => '^platnosc-status$', 'destination' => 'index.php?module=shop_order&action=payment_status_hotpay' ], + [ 'pattern' => '^przelewy24-status$', 'destination' => 'index.php?module=shop_order&action=payment_status_przelewy24pl' ], + // Koszyk + [ 'pattern' => '^koszyk$', 'destination' => 'index.php?module=shop_basket&action=main_view' ], + [ 'pattern' => '^koszyk-podsumowanie$', 'destination' => 'index.php?module=shop_basket&action=summary_view' ], + [ 'pattern' => '^zloz-zamowienie$', 'destination' => 'index.php?module=shop_basket&action=basket_save' ], + // Klient + [ 'pattern' => '^rejestracja$', 'destination' => 'index.php?module=shop_client&action=register_form' ], + [ 'pattern' => '^logowanie$', 'destination' => 'index.php?module=shop_client&action=login_form' ], + [ 'pattern' => '^wylogowanie$', 'destination' => 'index.php?module=shop_client&action=logout' ], + [ 'pattern' => '^odzyskiwanie-hasla$', 'destination' => 'index.php?module=shop_client&action=recover_password' ], + [ 'pattern' => '^panel-klienta/zamowienia$', 'destination' => 'index.php?module=shop_client&action=client_orders' ], + [ 'pattern' => '^panel-klienta/adresy$', 'destination' => 'index.php?module=shop_client&action=client_addresses' ], + [ 'pattern' => '^panel-klienta/nowy-adres$', 'destination' => 'index.php?module=shop_client&action=address_edit' ], + [ 'pattern' => '^panel-klienta/edytuj-adres/([0-9]+)$', 'destination' => 'index.php?module=shop_client&action=address_edit&id=$1' ], + [ 'pattern' => '^panel-klienta/usun-adres/([0-9]+)$', 'destination' => 'index.php?module=shop_client&action=address_delete&id=$1' ], + // Newsletter + [ 'pattern' => '^newsletter/signin$', 'destination' => 'index.php?module=newsletter&action=signin' ], + [ 'pattern' => '^newsletter/confirm/hash=(.+)$', 'destination' => 'index.php?module=newsletter&action=confirm&hash=$1' ], + [ 'pattern' => '^newsletter/unsubscribe/hash=(.+)$', 'destination' => 'index.php?module=newsletter&action=unsubscribe&hash=$1' ], + // Moduły AJAX (shopBasket, shopClient, shopProduct, shopCoupon, search) + [ 'pattern' => '^shopBasket/([^/]+)/(.+)$', 'destination' => 'index.php?module=shopBasket&action=$1&$2' ], + [ 'pattern' => '^shopBasket/([^/]+)$', 'destination' => 'index.php?module=shopBasket&action=$1' ], + [ 'pattern' => '^shopClient/([^/]+)/(.+)$', 'destination' => 'index.php?module=shopClient&action=$1&$2' ], + [ 'pattern' => '^shopClient/([^/]+)$', 'destination' => 'index.php?module=shopClient&action=$1' ], + [ 'pattern' => '^shopProduct/([^/]+)/(.+)$', 'destination' => 'index.php?module=shopProduct&action=$1&$2' ], + [ 'pattern' => '^shopProduct/([^/]+)$', 'destination' => 'index.php?module=shopProduct&action=$1' ], + [ 'pattern' => '^shopCoupon/([^/]+)/(.+)$', 'destination' => 'index.php?module=shopCoupon&action=$1&$2' ], + [ 'pattern' => '^shopCoupon/([^/]+)$', 'destination' => 'index.php?module=shopCoupon&action=$1' ], + [ 'pattern' => '^search/([^/]+)/(.+)$', 'destination' => 'index.php?module=search&action=$1&$2' ], + [ 'pattern' => '^search/([^/]+)$', 'destination' => 'index.php?module=search&action=$1' ], + ]; + + foreach ( $systemRoutes as $route ) + { + $mdb->insert( 'pp_routes', [ + 'type' => 'system', + 'lang_id' => 0, + 'pattern' => $route['pattern'], + 'destination' => $route['destination'], + ] ); + } + + // Dynamic system routes — languages + $results = $mdb->select( 'pp_langs', [ 'id' ], [ 'status' => 1, 'ORDER' => [ 'o' => 'ASC' ] ] ); + if ( is_array( $results ) ) foreach ( $results as $row ) + { + $mdb->insert( 'pp_routes', [ + 'type' => 'system', + 'lang_id' => 0, + 'pattern' => '^' . $row['id'] . '$', + 'destination' => 'index.php?a=change_language&id=' . $row['id'], + ] ); + } + + // Dynamic system routes — producenci + $categoryDefaultLayoutId = ( new \Domain\Layouts\LayoutsRepository( $mdb ) )->categoryDefaultLayoutId(); + + $mdb->insert( 'pp_routes', [ + 'type' => 'system', + 'lang_id' => 0, + 'pattern' => '^producenci$', + 'destination' => 'index.php?module=shop_producer&action=list&layout_id=' . $categoryDefaultLayoutId, + ] ); + + $rows = $mdb->select( 'pp_shop_producer', '*', [ 'status' => 1 ] ); + if ( self::is_array_fix( $rows ) ) foreach ( $rows as $row ) + { + $mdb->insert( 'pp_routes', [ + 'type' => 'system', + 'lang_id' => 0, + 'pattern' => '^producent/' . self::seo( $row['name'] ) . '$', + 'destination' => 'index.php?module=shop_producer&action=products&producer_id=' . $row['id'] . '&layout_id=' . $categoryDefaultLayoutId, + ] ); + $mdb->insert( 'pp_routes', [ + 'type' => 'system', + 'lang_id' => 0, + 'pattern' => '^producent/' . self::seo( $row['name'] ) . '/([0-9]+)$', + 'destination' => 'index.php?module=shop_producer&action=products&producer_id=' . $row['id'] . '&layout_id=' . $categoryDefaultLayoutId . '&bs=$1', + ] ); + } + + // + // HTACCESS — generuj z PHP (bez szablonu htaccess.conf) + // + $htaccess_data = 'RewriteEngine On' . PHP_EOL; + $htaccess_data .= 'RewriteBase /' . PHP_EOL; + $htaccess_data .= 'Options +FollowSymlinks' . PHP_EOL; + $htaccess_data .= 'Options -Indexes' . PHP_EOL; + $htaccess_data .= PHP_EOL; + $htaccess_data .= '# Przekierowanie z www na bez www i z http na https w jednym kroku' . PHP_EOL; + $htaccess_data .= 'RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]' . PHP_EOL; + $htaccess_data .= 'RewriteRule ^ https://%1%{REQUEST_URI} [R=301,L]' . PHP_EOL; + $htaccess_data .= PHP_EOL; + $htaccess_data .= '# Przekierowanie z http na https, jesli nie zawiera www' . PHP_EOL; + $htaccess_data .= 'RewriteCond %{HTTPS} off' . PHP_EOL; + $htaccess_data .= 'RewriteCond %{REQUEST_URI} !^/(tpay-status|platnosc-status|przelewy24-status)$ [NC]' . PHP_EOL; + $htaccess_data .= 'RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]' . PHP_EOL; + $htaccess_data .= PHP_EOL; + $htaccess_data .= '# Usuwanie koncowego slasha dla niekatalogów' . PHP_EOL; + $htaccess_data .= 'RewriteCond %{REQUEST_FILENAME} !-d' . PHP_EOL; + $htaccess_data .= 'RewriteCond %{REQUEST_URI} !^/admin/.*$ [NC]' . PHP_EOL; + $htaccess_data .= 'RewriteCond %{REQUEST_URI} (.+)/$' . PHP_EOL; + $htaccess_data .= 'RewriteRule ^ %1 [R=301,L]' . PHP_EOL; + $htaccess_data .= PHP_EOL; + $htaccess_data .= 'RewriteCond %{REQUEST_URI} !^(.*)/libraries/(.*) [NC]' . PHP_EOL; + $htaccess_data .= 'RewriteCond %{REQUEST_URI} !^(.*)/layout/(.*) [NC]' . PHP_EOL; + $htaccess_data .= 'RewriteRule ^admin/([^/]*)/([^/]*)/(.*)$ admin/index.php?module=$1&action=$2&$3 [L]' . PHP_EOL; + $htaccess_data .= PHP_EOL; + $htaccess_data .= 'RewriteRule ^admin/$ admin/index.php [L]' . PHP_EOL; + $htaccess_data .= PHP_EOL; + $htaccess_data .= 'RewriteRule ^thumb/([0-9]*)/([0-9]*)/(.*)$ /libraries/thumb.php?img=$3&w=$1&h=$2 [L]' . PHP_EOL; + $htaccess_data .= PHP_EOL; + $htaccess_data .= 'RewriteCond %{THE_REQUEST} ^[A-Z]{3,9}\ /index.php' . PHP_EOL; + $htaccess_data .= 'RewriteRule ^ /%1 [R=301,L]' . PHP_EOL; + + /* cache — zastąpienie placeholdera {HTACCESS_CACHE} */ + if ( $settings['htaccess_cache'] ) + { + $htaccess_data .= '' . PHP_EOL + . 'AddOutputFilterByType DEFLATE text/html text/plain text/xml application/xml application/xhtml+xml text/css text/javascript application/javascript application/x-javascript' . PHP_EOL + . '' . PHP_EOL + . '' . PHP_EOL + . 'Header set Access-Control-Allow-Origin "*"' . PHP_EOL + . '' . PHP_EOL + . '' . PHP_EOL + . 'ExpiresActive on' . PHP_EOL + . 'ExpiresDefault "access plus 1 month"' . PHP_EOL + . 'ExpiresByType text/css "access plus 1 year"' . PHP_EOL + . 'ExpiresByType application/json "access plus 0 seconds"' . PHP_EOL + . 'ExpiresByType application/xml "access plus 0 seconds"' . PHP_EOL + . 'ExpiresByType text/xml "access plus 0 seconds"' . PHP_EOL + . 'ExpiresByType image/x-icon "access plus 1 week"' . PHP_EOL + . 'ExpiresByType text/x-component "access plus 1 month"' . PHP_EOL + . 'ExpiresByType text/html "access plus 0 seconds"' . PHP_EOL + . 'ExpiresByType application/javascript "access plus 1 year"' . PHP_EOL + . 'ExpiresByType application/x-web-app-manifest+json "access plus 0 seconds"' . PHP_EOL + . 'ExpiresByType text/cache-manifest "access plus 0 seconds"' . PHP_EOL + . 'ExpiresByType audio/ogg "access plus 1 month"' . PHP_EOL + . 'ExpiresByType image/gif "access plus 1 month"' . PHP_EOL + . 'ExpiresByType image/jpeg "access plus 1 month"' . PHP_EOL + . 'ExpiresByType image/png "access plus 1 month"' . PHP_EOL + . 'ExpiresByType video/mp4 "access plus 1 month"' . PHP_EOL + . 'ExpiresByType video/ogg "access plus 1 month"' . PHP_EOL + . 'ExpiresByType video/webm "access plus 1 month"' . PHP_EOL + . 'ExpiresByType application/atom+xml "access plus 1 hour"' . PHP_EOL + . 'ExpiresByType application/rss+xml "access plus 1 hour"' . PHP_EOL + . 'ExpiresByType application/font-woff "access plus 1 month"' . PHP_EOL + . 'ExpiresByType application/vnd.ms-fontobject "access plus 1 month"' . PHP_EOL + . 'ExpiresByType application/x-font-ttf "access plus 1 month"' . PHP_EOL + . 'ExpiresByType font/opentype "access plus 1 month"' . PHP_EOL + . 'ExpiresByType image/svg+xml "access plus 1 month"' . PHP_EOL + . '' . PHP_EOL; + } + else + { + $htaccess_data .= '' . PHP_EOL + . 'Header set Cache-Control "no-cache, no-store, must-revalidate"' . PHP_EOL + . 'Header set Pragma "no-cache"' . PHP_EOL + . 'Header set Expires 0' . PHP_EOL + . '' . PHP_EOL; + } + + $htaccess_data .= '' . PHP_EOL; + $htaccess_data .= ' Order Deny,Allow' . PHP_EOL; + $htaccess_data .= ' Deny from all' . PHP_EOL; + $htaccess_data .= '' . PHP_EOL; + $htaccess_data .= '' . PHP_EOL; + $htaccess_data .= ' Order Deny,Allow' . PHP_EOL; + $htaccess_data .= ' Deny from all' . PHP_EOL; + $htaccess_data .= '' . PHP_EOL; + $htaccess_data .= '' . PHP_EOL; + $htaccess_data .= ' Order Deny,Allow' . PHP_EOL; + $htaccess_data .= ' Deny from all' . PHP_EOL; + $htaccess_data .= '' . PHP_EOL; + + // + // KATEGORIE — sitemap + pp_routes (bez zmian) + // + $results = $mdb->select( 'pp_langs', [ 'id', 'start' ], [ 'status' => 1, 'ORDER' => [ 'o' => 'ASC' ] ] ); + if ( is_array( $results ) ) foreach ( $results as $row ) + { + !$row['start'] ? $language_link = $row['id'] . '/' : $language_link = ''; + + $results2 = $mdb->select( 'pp_shop_categories_langs', [ '[><]pp_shop_categories' => [ 'category_id' => 'id' ] ], [ 'seo_link', 'title', 'category_id' ], [ 'lang_id' => $row['id'], 'ORDER' => [ 'o' => 'ASC' ] ] ); + if ( is_array( $results2 ) ) foreach ( $results2 as $row2 ) + { + if ( $row2['title'] ) + { + $site_map .= '' . PHP_EOL; + if ( $row2['seo_link'] ) + $site_map .= 'https://' . $url . '/' . $language_link . self::seo( $row2['seo_link'] ) . '' . PHP_EOL; + else + $site_map .= 'https://' . $url . '/' . $language_link . 'k-' . $row2['category_id'] . '-' . self::seo( $row2['title'] ) . '' . PHP_EOL; + $site_map .= '' . date( 'Y-m-d' ) . '' . PHP_EOL; + $site_map .= 'daily' . PHP_EOL; + $site_map .= '1' . PHP_EOL; + $site_map .= '' . PHP_EOL; + + $seoSlug = $row2['seo_link'] ? self::seo( $row2['seo_link'] ) : 'k-' . $row2['category_id'] . '-' . self::seo( $row2['title'] ); + + $mdb->delete( 'pp_routes', [ 'AND' => [ 'category_id' => $row2['category_id'], 'lang_id' => $row['id'] ] ] ); + + $mdb->insert( 'pp_routes', [ + 'category_id' => $row2['category_id'], + 'lang_id' => $row['id'], + 'pattern' => '^' . $language_link . $seoSlug . '$', + 'destination' => 'index.php?category=' . $row2['category_id'] . '&lang=' . $row['id'], + ] ); + $mdb->insert( 'pp_routes', [ + 'category_id' => $row2['category_id'], + 'lang_id' => $row['id'], + 'pattern' => '^' . $language_link . $seoSlug . '/([0-9]+)$', + 'destination' => 'index.php?category=' . $row2['category_id'] . '&lang=' . $row['id'] . '&bs=$1', + ] ); + } + } + } + + // + // PRODUKTY — sitemap + pp_routes (bez zmian) + // + $results = $mdb->select( 'pp_langs', [ 'id', 'start' ], [ 'status' => 1, 'ORDER' => [ 'o' => 'ASC' ] ] ); + if ( is_array( $results ) ) + { + foreach ( $results as $row ) + { + !$row['start'] ? $language_link = $row['id'] . '/' : $language_link = ''; + + $results2 = $mdb->select( 'pp_shop_products_langs', [ '[><]pp_shop_products' => [ 'product_id' => 'id' ] ], [ 'seo_link', 'name', 'product_id' ], [ 'lang_id' => $row['id'], 'ORDER' => [ 'name' => 'ASC' ] ] ); + if ( is_array( $results2 ) ) + { + foreach ( $results2 as $row2 ) + { + $mdb->delete( 'pp_routes', [ 'AND' => [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'] ] ] ); + + if ( $row2['name'] ) + { + $site_map .= '' . PHP_EOL; + if ( $row2['seo_link'] ) + $site_map .= 'https://' . $url . '/' . $language_link . self::seo( $row2['seo_link'] ) . '' . PHP_EOL; + else + $site_map .= 'https://' . $url . '/' . $language_link . 'p-' . $row2['product_id'] . '-' . self::seo( $row2['name'] ) . '' . PHP_EOL; + $site_map .= '' . date( 'Y-m-d' ) . '' . PHP_EOL; + $site_map .= 'daily' . PHP_EOL; + $site_map .= '1' . PHP_EOL; + $site_map .= '' . PHP_EOL; + + if ( $row2['seo_link'] ) + { + $mdb->insert( 'pp_routes', [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'], 'pattern' => '^' . $language_link . self::seo( $row2['seo_link'] ) . '$', 'destination' => 'index.php?product=' . $row2['product_id'] ] ); + $mdb->insert( 'pp_routes', [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'], 'pattern' => '^' . $language_link . self::seo( $row2['seo_link'] ) . '/([0-9-]+)$', 'destination' => 'index.php?product=' . $row2['product_id'] . '&permutation_hash=$1' ] ); + } + else + { + $mdb->insert( 'pp_routes', [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'], 'pattern' => '^' . $language_link . 'p-' . $row2['product_id'] . '-' . self::seo( $row2['name'] ) . '$', 'destination' => 'index.php?product=' . $row2['product_id'] ] ); + $mdb->insert( 'pp_routes', [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'], 'pattern' => '^' . $language_link . 'p-' . $row2['product_id'] . '-' . self::seo( $row2['name'] ) . '/([0-9-]+)$', 'destination' => 'index.php?product=' . $row2['product_id'] . '&permutation_hash=$1' ] ); + } + } + } + } + } + } + + // + // STRONY + ARTYKULY — sitemap + pp_routes (bez zmian) + // + $results = $mdb->select( 'pp_langs', [ 'id', 'start' ], [ 'status' => 1, 'ORDER' => [ 'o' => 'ASC' ] ] ); + if ( is_array( $results ) ) + foreach ( $results as $row ) + { + ( !$row['start'] and count( $results ) > 1 ) ? $language_link = $row['id'] . '/' : $language_link = ''; + + $results2 = $mdb->select( 'pp_pages_langs', [ '[><]pp_pages' => [ 'page_id' => 'id' ] ], [ 'seo_link', 'title', 'page_id', 'noindex', 'start', 'link', 'page_type' ], [ 'lang_id' => $row['id'], 'ORDER' => [ 'start' => 'DESC', 'o' => 'ASC' ] ] ); + if ( is_array( $results2 ) ) + foreach ( $results2 as $row2 ) + { + if ( $row2['title'] and $row2['page_type'] != 3 and $row2['page_type'] != 5 ) + { + if ( !$row2['noindex'] ) + { + $site_map .= '' . PHP_EOL; + if ( $row2['seo_link'] ) + $site_map .= 'https://' . $url . '/' . self::seo( $row2['seo_link'] ) . '' . PHP_EOL; + else + $site_map .= 'https://' . $url . '/s-' . $row2['page_id'] . '-' . self::seo( $row2['title'] ) . '' . PHP_EOL; + $site_map .= '' . date( 'Y-m-d' ) . '' . PHP_EOL; + $site_map .= 'daily' . PHP_EOL; + $site_map .= '1' . PHP_EOL; + $site_map .= '' . PHP_EOL; + } + else if ( $row2['noindex'] and $row2['seo_link'] ) + { + $robots .= 'User-agent: GoogleBot' . PHP_EOL; + $robots .= 'Disallow: /' . $row2['seo_link'] . PHP_EOL; + } + + if ( $row2['start'] ) + { + if ( $row2['seo_link'] ) + { + $htaccess_data .= PHP_EOL . 'RewriteCond %{REQUEST_URI} ^/' . self::seo( $row2['seo_link'] ) . '$'; + $htaccess_data .= PHP_EOL . 'RewriteRule ^(.*)$ http://www.' . $url . '/' . $language_link . ' [R=permanent,L]'; + $htaccess_data .= PHP_EOL . 'RewriteCond %{REQUEST_URI} ^/' . self::seo( $row2['seo_link'] ) . '-1$'; + $htaccess_data .= PHP_EOL . 'RewriteRule ^(.*)$ http://www.' . $url . '/' . $language_link . ' [R=permanent,L]'; + } + else + { + $htaccess_data .= PHP_EOL . 'RewriteCond %{REQUEST_URI} ^/s-' . $row2['page_id'] . '-' . self::seo( $row2['title'] ) . '$'; + $htaccess_data .= PHP_EOL . 'RewriteRule ^(.*)$ http://www.' . $url . '/' . $language_link . ' [R=permanent,L]'; + $htaccess_data .= PHP_EOL . 'RewriteCond %{REQUEST_URI} ^/s-' . $row2['page_id'] . '-' . self::seo( $row2['title'] ) . '-1$'; + $htaccess_data .= PHP_EOL . 'RewriteRule ^(.*)$ http://www.' . $url . '/' . $language_link . ' [R=permanent,L]'; + } + $htaccess_data .= PHP_EOL . 'RewriteRule ^$ index.php?a=page&id=' . $row2['page_id'] . '&lang=' . $row['id'] . ' [L]'; + } + + $seoSlug = $row2['seo_link'] ? self::seo( $row2['seo_link'] ) : 's-' . $row2['page_id'] . '-' . self::seo( $row2['title'] ); + $langPrefix = $row2['start'] ? '' : $language_link; + + $mdb->delete( 'pp_routes', [ 'AND' => [ 'page_id' => $row2['page_id'], 'lang_id' => $row['id'] ] ] ); + + $mdb->insert( 'pp_routes', [ + 'page_id' => $row2['page_id'], + 'lang_id' => $row['id'], + 'pattern' => '^' . $langPrefix . $seoSlug . '$', + 'destination' => 'index.php?a=page&id=' . $row2['page_id'] . '&lang=' . $row['id'], + ] ); + $mdb->insert( 'pp_routes', [ + 'page_id' => $row2['page_id'], + 'lang_id' => $row['id'], + 'pattern' => '^' . $langPrefix . $seoSlug . '/([0-9]+)$', + 'destination' => 'index.php?a=page&id=' . $row2['page_id'] . '&lang=' . $row['id'] . '&bs=$1', + ] ); + } + } + + $results2 = $mdb->select( 'pp_articles_langs', [ '[><]pp_articles' => [ 'article_id' => 'id' ] ], [ 'seo_link', 'title', 'article_id', 'noindex', 'copy_from' ], [ 'AND' => [ 'status' => 1, 'lang_id' => $row['id'], 'block_direct_access' => 0 ] ] ); + if ( is_array( $results2 ) ) + foreach ( $results2 as $row2 ) + { + if ( $row2['copy_from'] != null ) + { + $results_tmp = $mdb->get( 'pp_articles_langs', [ 'seo_link', 'title' ], [ 'AND' => [ 'article_id' => $row2['article_id'], 'lang_id' => $row2['copy_from'] ] ] ); + $row2['seo_link'] = $results_tmp['seo_link']; + $row2['title'] = $results_tmp['title']; + } + + if ( !$row2['noindex'] ) + { + $site_map .= '' . PHP_EOL; + if ( $row2['seo_link'] ) + $site_map .= 'https://' . $url . '/' . self::seo( $row2['seo_link'] ) . '' . PHP_EOL; + else + $site_map .= 'https://' . $url . '/a-' . $row2['article_id'] . '-' . self::seo( $row2['title'] ) . '' . PHP_EOL; + $site_map .= '' . date( 'Y-m-d' ) . '' . PHP_EOL; + $site_map .= 'daily' . PHP_EOL; + $site_map .= '1' . PHP_EOL; + $site_map .= '' . PHP_EOL; + } + else if ( $row2['noindex'] and $row2['seo_link'] ) + { + $robots .= 'User-agent: GoogleBot' . PHP_EOL; + $robots .= 'Disallow: /' . $row2['seo_link'] . PHP_EOL; + } + + $mdb->delete( 'pp_routes', [ 'AND' => [ 'article_id' => $row2['article_id'], 'lang_id' => $row['id'] ] ] ); + + if ( $row2['seo_link'] ) + { + $mdb->insert( 'pp_routes', [ + 'article_id' => $row2['article_id'], + 'lang_id' => $row['id'], + 'pattern' => '^' . $language_link . self::seo( $row2['seo_link'] ) . '$', + 'destination' => 'index.php?article=' . $row2['article_id'] . '&lang=' . $row['id'], + ] ); + } + else if ( $row2['title'] != null ) + { + $mdb->insert( 'pp_routes', [ + 'article_id' => $row2['article_id'], + 'lang_id' => $row['id'], + 'pattern' => '^' . $language_link . 'a-' . $row2['article_id'] . '-' . self::seo( $row2['title'] ) . '$', + 'destination' => 'index.php?article=' . $row2['article_id'] . '&lang=' . $row['id'], + ] ); + } + } + } + + // Invalidacja cache tras + try { + ( new \Shared\Cache\CacheHandler() )->delete( 'pp_routes:all' ); + } catch ( \Exception $e ) { + // Redis niedostepny — ignorujemy + } + + $results = $mdb->get( 'pp_settings', 'value', [ 'param' => 'htaccess' ] ); + if ( $results ) + $htaccess_data .= PHP_EOL . $results; + + $results = $mdb->get( 'pp_settings', 'value', [ 'param' => 'robots' ] ); + if ( $results ) + $robots .= PHP_EOL . $results; + + $site_map .= ''; + + $htaccess_data .= PHP_EOL; + $htaccess_data .= 'RewriteCond %{REQUEST_FILENAME} !-f' . PHP_EOL; + $htaccess_data .= 'RewriteCond %{REQUEST_FILENAME} !-d' . PHP_EOL; + $htaccess_data .= 'RewriteRule ^ index.php [L]'; + + // Niektore hostingi blokuja zmiane wersji PHP przez .htaccess. + $htaccess_data = preg_replace( '/^(\\s*)(AddHandler|SetHandler|ForceType)\\b/im', '$1# $2', $htaccess_data ); + + $fp = fopen( $dir . '.htaccess', 'w' ); + fwrite( $fp, $htaccess_data ); + fclose( $fp ); + + $fp = fopen( $dir . 'sitemap.xml', 'w' ); + fwrite( $fp, $site_map ); + fclose( $fp ); + + $fp = fopen( $dir . 'robots.txt', 'w' ); + fwrite( $fp, $robots ); + fclose( $fp ); + } +``` + +**Step 2: Run tests** +``` +php phpunit.phar --configuration phpunit.xml +``` +Expected: all tests pass (htacces() has no unit tests, covered by integration). + +--- + +## Task 3: Delete `libraries/htaccess.conf` + +**Files:** +- Delete: `libraries/htaccess.conf` + +**Step 1: Verify htacces() no longer references the file** + +Search for any remaining `file_get_contents` referencing `htaccess.conf`: +```bash +grep -r "htaccess.conf" autoload/ +``` +Expected: no results. + +**Step 2: Delete the file** +```bash +rm libraries/htaccess.conf +``` + +**Step 3: Run tests** +``` +php phpunit.phar --configuration phpunit.xml +``` +Expected: all tests still pass. + +--- + +## Task 4: Update `docs/DATABASE_STRUCTURE.md` + +**Files:** +- Modify: `docs/DATABASE_STRUCTURE.md` (section `## pp_routes`) + +**Step 1: Add `type` column to the pp_routes table description** + +Find the `## pp_routes` section and add the `type` row to the column table: + +```markdown +| type | Typ trasy: NULL = encja (produkt/kategoria/strona/artykuł), 'system' = trasa systemowa | +``` + +Also update the description paragraph to mention that system routes are managed automatically. + +--- + +## Task 5: Manual integration test on server + +**Step 1: Apply migration** +```sql +ALTER TABLE pp_routes ADD COLUMN type VARCHAR(20) NULL AFTER article_id; +``` + +**Step 2: Trigger `htacces()` regeneration** + +Log in to admin panel → save any product or category → this calls `htacces()`. + +**Step 3: Verify pp_routes has system routes** +```sql +SELECT COUNT(*) FROM pp_routes WHERE type = 'system'; +``` +Expected: ~35+ rows (32 static + language rows + producer rows). + +**Step 4: Verify .htaccess was generated correctly** + +Open `.htaccess` — should NOT contain `RewriteRule ^koszyk$`, `^logowanie$`, etc. Should contain HTTPS redirect, admin routing, thumb routing, cache block. + +**Step 5: Test URLs in browser** +- `/koszyk` → koszyk page ✓ +- `/logowanie` → login page ✓ +- `/wyszukiwarka/test` → search results ✓ +- `/zamowienie/abc123` → order details ✓ +- `/shopClient/confirm/hash=xyz` → client confirm action ✓ +- Category URL → category page ✓ +- Product URL → product page ✓ + +**Step 6: Run full test suite** +``` +php phpunit.phar --configuration phpunit.xml +``` +Expected: 807 tests, all pass. + +--- + +## Task 6: Commit + +**Step 1: Stage and commit** +```bash +git add migrations/0.329.sql +git add autoload/Shared/Helpers/Helpers.php +git add docs/DATABASE_STRUCTURE.md +git add docs/plans/2026-02-27-htaccess-conf-elimination.md +git add docs/plans/2026-02-27-htaccess-to-routes-design.md +git rm libraries/htaccess.conf +git commit -m "feat: eliminate htaccess.conf, move all routes to pp_routes (v0.330)" +``` diff --git a/docs/plans/2026-02-27-htaccess-to-routes-design.md b/docs/plans/2026-02-27-htaccess-to-routes-design.md new file mode 100644 index 0000000..93bc51b --- /dev/null +++ b/docs/plans/2026-02-27-htaccess-to-routes-design.md @@ -0,0 +1,121 @@ +# Design: Eliminacja htaccess.conf i przeniesienie wszystkich tras do pp_routes + +**Data:** 2026-02-27 +**Wersja docelowa:** 0.330 + +--- + +## Cel + +Wyeliminowanie pliku `libraries/htaccess.conf` jako szablonu i przeniesienie wszystkich URL-i, które dotychczas były wpisane na sztywno w `.htaccess`, do tabeli `pp_routes`. Logika generowania `.htaccess` zostaje w całości w `Helpers::htacces()`. + +--- + +## Co zostaje w `.htaccess` (reguły Apache-level) + +Tylko dyrektywy, których PHP nie może obsłużyć: + +- `RewriteEngine On`, `Options` +- Redirect HTTPS/www +- Redirect HTTP→HTTPS (z wyłączeniem tpay-status, platnosc-status, przelewy24-status) +- Usuwanie trailing slash (z wyłączeniem `/admin/`) +- Routing `/admin/` → `admin/index.php` +- `thumb/([0-9]*)/([0-9]*)/(.*)` → `/libraries/thumb.php` (inny plik PHP — niemożliwe przez pp_routes) +- `RewriteCond %{THE_REQUEST} ^[A-Z]{3,9}\ /index.php` — redirect z index.php +- Blok cache headers (gzip, expires) — zależny od `$settings['htaccess_cache']` +- Ochrona plików: ``, ``, `` +- Przekierowania 301 stron startowych (generowane dynamicznie w pętli pages) +- Niestandardowe reguły z `pp_settings` (param=htaccess) +- Catch-all: `RewriteCond !-f`, `!-d`, `RewriteRule ^ index.php [L]` + +--- + +## Co przechodzi do `pp_routes` + +### Statyczne trasy systemowe (hardcoded, niezmienne) + +| Pattern | Destination | +|---------|-------------| +| `^wyszukiwarka/([^/]+)/([0-9]+)$` | `index.php?module=search&action=search_results&query=$1&bs=$2` | +| `^wyszukiwarka/([^/]+)$` | `index.php?module=search&action=search_results&query=$1&bs=1` | +| `^zamowienie/([a-zA-Z0-9-]+)$` | `index.php?module=shop_order&action=order_details&order_hash=$1` | +| `^potwierdzenie-platnosci/([a-zA-Z0-9-]+)$` | `index.php?module=shop_order&action=payment_confirmation&order_hash=$1` | +| `^tpay-status$` | `index.php?module=shop_order&action=payment_status_tpay` | +| `^platnosc-status$` | `index.php?module=shop_order&action=payment_status_hotpay` | +| `^przelewy24-status$` | `index.php?module=shop_order&action=payment_status_przelewy24pl` | +| `^koszyk$` | `index.php?module=shop_basket&action=main_view` | +| `^koszyk-podsumowanie$` | `index.php?module=shop_basket&action=summary_view` | +| `^zloz-zamowienie$` | `index.php?module=shop_basket&action=basket_save` | +| `^rejestracja$` | `index.php?module=shop_client&action=register_form` | +| `^logowanie$` | `index.php?module=shop_client&action=login_form` | +| `^wylogowanie$` | `index.php?module=shop_client&action=logout` | +| `^odzyskiwanie-hasla$` | `index.php?module=shop_client&action=recover_password` | +| `^panel-klienta/zamowienia$` | `index.php?module=shop_client&action=client_orders` | +| `^panel-klienta/adresy$` | `index.php?module=shop_client&action=client_addresses` | +| `^panel-klienta/nowy-adres$` | `index.php?module=shop_client&action=address_edit` | +| `^panel-klienta/edytuj-adres/([0-9]+)$` | `index.php?module=shop_client&action=address_edit&id=$1` | +| `^panel-klienta/usun-adres/([0-9]+)$` | `index.php?module=shop_client&action=address_delete&id=$1` | +| `^newsletter/signin$` | `index.php?module=newsletter&action=signin` | +| `^newsletter/confirm/hash=(.+)$` | `index.php?module=newsletter&action=confirm&hash=$1` | +| `^newsletter/unsubscribe/hash=(.+)$` | `index.php?module=newsletter&action=unsubscribe&hash=$1` | + +### Trasy modułów AJAX (shopBasket, shopClient, shopProduct, shopCoupon, search) + +Dwa wzorce na moduł — 3-segmentowy (z parametrami) i 2-segmentowy: + +| Pattern | Destination | +|---------|-------------| +| `^shopBasket/([^/]+)/(.+)$` | `index.php?module=shopBasket&action=$1&$2` | +| `^shopBasket/([^/]+)$` | `index.php?module=shopBasket&action=$1` | +| `^shopClient/([^/]+)/(.+)$` | `index.php?module=shopClient&action=$1&$2` | +| `^shopClient/([^/]+)$` | `index.php?module=shopClient&action=$1` | +| `^shopProduct/([^/]+)/(.+)$` | `index.php?module=shopProduct&action=$1&$2` | +| `^shopProduct/([^/]+)$` | `index.php?module=shopProduct&action=$1` | +| `^shopCoupon/([^/]+)/(.+)$` | `index.php?module=shopCoupon&action=$1&$2` | +| `^shopCoupon/([^/]+)$` | `index.php?module=shopCoupon&action=$1` | +| `^search/([^/]+)/(.+)$` | `index.php?module=search&action=$1&$2` | +| `^search/([^/]+)$` | `index.php?module=search&action=$1` | + +### Dynamiczne trasy systemowe (wstawiane przy każdym `htacces()`) + +- **Języki:** `^{lang_id}$` → `index.php?a=change_language&id={lang_id}` (per każdy aktywny język) +- **Producenci lista:** `^producenci$` → `index.php?module=shop_producer&action=list&layout_id={id}` +- **Producent detail:** `^producent/{slug}$` i `^producent/{slug}/([0-9]+)$` (per producent z DB) + +--- + +## Nowa kolumna `type` w `pp_routes` + +```sql +ADD COLUMN type VARCHAR(20) NULL AFTER article_id +``` + +| Wartość | Znaczenie | +|---------|-----------| +| `NULL` | Trasa encji (produkt, kategoria, strona, artykuł) | +| `'system'` | Trasa systemowa (wszystkie powyższe) | + +**Zarządzanie:** przy każdym `htacces()`: +```php +$mdb->delete('pp_routes', ['type' => 'system']); // usuń wszystkie +// ... wstaw na nowo (statyczne + dynamiczne) +``` + +--- + +## Eliminacja `htaccess.conf` + +`file_get_contents($dir . 'libraries/htaccess.conf')` zastąpione PHP stringiem z tą samą treścią (tylko Apache-level reguły). Placeholder `{HTACCESS_CACHE}` zastąpiony bezpośrednim `if ($settings['htaccess_cache']) { ... } else { ... }` wbudowanym w odpowiednim miejscu. + +Plik `libraries/htaccess.conf` zostaje usunięty. + +--- + +## Pliki do modyfikacji + +| Plik | Zmiana | +|------|--------| +| `migrations/0.329.sql` | Dodać `ADD COLUMN type VARCHAR(20) NULL` | +| `Helpers::htacces()` | Usunąć `file_get_contents`, wbudować statyczny header, dodać inserty system routes, usunąć htaccess rules dla języków/newsletter/producenci | +| `libraries/htaccess.conf` | Usunąć plik | +| `docs/DATABASE_STRUCTURE.md` | Dodać kolumnę `type` do opisu pp_routes | diff --git a/index.php b/index.php index 8888eae..3c0d695 100644 --- a/index.php +++ b/index.php @@ -59,6 +59,49 @@ $mdb = new medoo( [ 'time_debug' => $database['time_debug'] ] ); +// check routes +$parsed_url = parse_url($_SERVER['REQUEST_URI']); +$request_uri = ltrim($parsed_url['path'], '/'); +$query_string = isset($parsed_url['query']) ? $parsed_url['query'] : ''; +parse_str($query_string, $query_params); + +if ($request_uri != '') +{ + $cache = new \Shared\Cache\CacheHandler(); + $cacheKey = 'pp_routes:all'; + $routesCached = $cache->get($cacheKey); + + if ($routesCached === false || $routesCached === null) + { + $routes = $mdb->select('pp_routes', '*'); + $cache->set($cacheKey, $routes, 86400); + } + else + { + $routes = unserialize($routesCached); + } + + foreach ($routes as $route) + { + $pattern = $route['pattern']; + $destination = $route['destination']; + + if (preg_match("#^" . $pattern . "#", $request_uri, $matches)) + { + // Replace placeholders in the destination with matches from the request URI + $destination = preg_replace("#^" . $pattern . "#", $destination, $request_uri); + + // Parse the destination string to extract GET parameters + parse_str(parse_url($destination, PHP_URL_QUERY), $destination_params); + + // Merge the destination params with query params from the URL + $_GET = array_merge($destination_params, $query_params); + + break; + } + } +} + \front\App::checkUrlParams(); $langRepo = new \Domain\Languages\LanguagesRepository( $mdb ); @@ -99,39 +142,6 @@ if ( $request_uri != '' ) } } -// check routes -$parsed_url = parse_url($_SERVER['REQUEST_URI']); -$request_uri = ltrim($parsed_url['path'], '/'); -$query_string = isset($parsed_url['query']) ? $parsed_url['query'] : ''; -parse_str($query_string, $query_params); - -if ($request_uri != '') -{ - $matched = false; - - $routes = $mdb->select('pp_routes', '*'); - foreach ($routes as $route) - { - $pattern = $route['pattern']; - $destination = $route['destination']; - - if (preg_match("#^" . $pattern . "#", $request_uri, $matches)) - { - // Replace placeholders in the destination with matches from the request URI - $destination = preg_replace("#^" . $pattern . "#", $destination, $request_uri); - - // Parse the destination string to extract GET parameters - parse_str(parse_url($destination, PHP_URL_QUERY), $destination_params); - - // Merge the destination params with query params from the URL - $_GET = array_merge($destination_params, $query_params); - - $matched = true; - break; - } - } -} - $pagesRepo = new \Domain\Pages\PagesRepository( $mdb ); if ( \Shared\Helpers\Helpers::get( 'a' ) == 'page' and \Shared\Helpers\Helpers::get( 'id' ) ) diff --git a/libraries/htaccess.conf b/libraries/htaccess.conf deleted file mode 100644 index 6a21d98..0000000 --- a/libraries/htaccess.conf +++ /dev/null @@ -1,84 +0,0 @@ -RewriteEngine On -RewriteBase / -Options +FollowSymlinks -Options -Indexes - -# Przekierowanie z www na bez www i z http na https w jednym kroku -RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC] -RewriteRule ^ https://%1%{REQUEST_URI} [R=301,L] - -# Przekierowanie z http na https, jeśli nie zawiera www -RewriteCond %{HTTPS} off -RewriteCond %{REQUEST_URI} !^/(tpay-status|platnosc-status|przelewy24-status)$ [NC] -RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] - -# Usuwanie końcowego slash'a dla niekatalogów -RewriteCond %{REQUEST_FILENAME} !-d -RewriteCond %{REQUEST_URI} !^/admin/.*$ [NC] # Wyklucza ścieżki rozpoczynające się od "admin/" -RewriteCond %{REQUEST_URI} (.+)/$ -RewriteRule ^ %1 [R=301,L] - -RewriteCond %{REQUEST_URI} !^(.*)/libraries/(.*) [NC] -RewriteCond %{REQUEST_URI} !^(.*)/layout/(.*) [NC] -RewriteRule ^admin/([^/]*)/([^/]*)/(.*)$ admin/index.php?module=$1&action=$2&$3 [L] - -RewriteRule ^admin/$ admin/index.php [L] - -RewriteRule ^wyszukiwarka/(.*)/([0-9]*)$ index.php?module=search&action=search_results&query=$1&bs=$2 [L] -RewriteRule ^wyszukiwarka/(.*)$ index.php?module=search&action=search_results&query=$1&bs=1 [L] -RewriteRule ^zamowienie/([a-zA-Z0-9-]*)$ index.php?module=shop_order&action=order_details&order_hash=$1 [L] -RewriteRule ^potwierdzenie-platnosci/([a-zA-Z0-9-]*)$ index.php?module=shop_order&action=payment_confirmation&order_hash=$1 [L] -RewriteRule ^tpay-status$ index.php?module=shop_order&action=payment_status_tpay [QSA,L] -RewriteRule ^platnosc-status$ index.php?module=shop_order&action=payment_status_hotpay [QSA,L] -RewriteRule ^przelewy24-status$ index.php?module=shop_order&action=payment_status_przelewy24pl [QSA,L] -RewriteRule ^koszyk$ index.php?module=shop_basket&action=main_view [L] -RewriteRule ^koszyk-podsumowanie$ index.php?module=shop_basket&action=summary_view [L] -RewriteRule ^zloz-zamowienie$ index.php?module=shop_basket&action=basket_save [L] -RewriteRule ^rejestracja$ index.php?module=shop_client&action=register_form [L] -RewriteRule ^logowanie$ index.php?module=shop_client&action=login_form [L] -RewriteRule ^wylogowanie$ index.php?module=shop_client&action=logout [L] -RewriteRule ^odzyskiwanie-hasla$ index.php?module=shop_client&action=recover_password [L] -RewriteRule ^panel-klienta/zamowienia$ index.php?module=shop_client&action=client_orders [L] -RewriteRule ^panel-klienta/adresy$ index.php?module=shop_client&action=client_addresses [L] -RewriteRule ^panel-klienta/nowy-adres$ index.php?module=shop_client&action=address_edit [L] -RewriteRule ^panel-klienta/edytuj-adres/([0-9]*)$ index.php?module=shop_client&action=address_edit&id=$1 [L] -RewriteRule ^panel-klienta/usun-adres/([0-9]*)$ index.php?module=shop_client&action=address_delete&id=$1 [L] -RewriteRule ^thumb/([0-9]*)/([0-9]*)/(.*)$ /libraries/thumb.php?img=$3&w=$1&h=$2 [L] - -RewriteCond %{REQUEST_URI} ^/shopBasket/(.*)/(.*) [NC] -RewriteRule ^([^/]*)/([^/]*)/(.*)$ index.php?module=$1&action=$2&$3 [L] -RewriteCond %{REQUEST_URI} ^/shopClient/(.*)/(.*) [NC] -RewriteRule ^([^/]*)/([^/]*)/(.*)$ index.php?module=$1&action=$2&$3 [L] -RewriteCond %{REQUEST_URI} ^/shopProduct/(.*)/(.*) [NC] -RewriteRule ^([^/]*)/([^/]*)/(.*)$ index.php?module=$1&action=$2&$3 [L] -RewriteCond %{REQUEST_URI} ^/shopCoupon/(.*)/(.*) [NC] -RewriteRule ^([^/]*)/([^/]*)/(.*)$ index.php?module=$1&action=$2&$3 [L] -RewriteCond %{REQUEST_URI} ^/search/(.*)/(.*) [NC] -RewriteRule ^([^/]*)/([^/]*)/(.*)$ index.php?module=$1&action=$2&$3 [L] - -RewriteCond %{REQUEST_URI} ^/shopBasket/(.*) [NC] -RewriteRule ^([^/]*)/([^/]*)$ index.php?module=$1&action=$2 [L] -RewriteCond %{REQUEST_URI} ^/shopClient/(.*) [NC] -RewriteRule ^([^/]*)/([^/]*)$ index.php?module=$1&action=$2 [L] -RewriteCond %{REQUEST_URI} ^/shopProduct/(.*) [NC] -RewriteRule ^([^/]*)/([^/]*)$ index.php?module=$1&action=$2 [L] -RewriteCond %{REQUEST_URI} ^/shopCoupon/(.*) [NC] -RewriteRule ^([^/]*)/([^/]*)$ index.php?module=$1&action=$2 [L] -RewriteCond %{REQUEST_URI} ^/search/(.*) [NC] -RewriteRule ^([^/]*)/([^/]*)$ index.php?module=$1&action=$2 [L] - -RewriteCond %{THE_REQUEST} ^[A-Z]{3,9}\ /index.php -RewriteRule ^ /%1 [R=301,L] -{HTACCESS_CACHE} - - Order Deny,Allow - Deny from all - - - Order Deny,Allow - Deny from all - - - Order Deny,Allow - Deny from all - diff --git a/migrations/0.329.sql b/migrations/0.329.sql new file mode 100644 index 0000000..eef9fc0 --- /dev/null +++ b/migrations/0.329.sql @@ -0,0 +1,5 @@ +ALTER TABLE pp_routes + ADD COLUMN category_id INT NULL AFTER product_id, + ADD COLUMN page_id INT NULL AFTER category_id, + ADD COLUMN article_id INT NULL AFTER page_id, + ADD COLUMN type VARCHAR(20) NULL AFTER article_id; diff --git a/tests/Unit/Domain/Article/ArticleRepositoryTest.php b/tests/Unit/Domain/Article/ArticleRepositoryTest.php index 1cfd039..070a69f 100644 --- a/tests/Unit/Domain/Article/ArticleRepositoryTest.php +++ b/tests/Unit/Domain/Article/ArticleRepositoryTest.php @@ -513,7 +513,7 @@ class ArticleRepositoryTest extends TestCase $mockDb = $this->createMock(\medoo::class); $deleteCalls = []; - $mockDb->expects($this->exactly(5)) + $mockDb->expects($this->exactly(6)) ->method('delete') ->willReturnCallback(function ($table, $where) use (&$deleteCalls) { $deleteCalls[] = ['table' => $table, 'where' => $where]; @@ -524,12 +524,13 @@ class ArticleRepositoryTest extends TestCase $result = $repository->deletePermanently(77); $this->assertTrue($result); - $this->assertCount(5, $deleteCalls); + $this->assertCount(6, $deleteCalls); $this->assertSame('pp_articles_pages', $deleteCalls[0]['table']); $this->assertSame('pp_articles_langs', $deleteCalls[1]['table']); $this->assertSame('pp_articles_images', $deleteCalls[2]['table']); $this->assertSame('pp_articles_files', $deleteCalls[3]['table']); - $this->assertSame('pp_articles', $deleteCalls[4]['table']); + $this->assertSame('pp_routes', $deleteCalls[4]['table']); + $this->assertSame('pp_articles', $deleteCalls[5]['table']); } public function testPagesSummaryForArticlesBuildsLabels(): void diff --git a/tests/Unit/Domain/Category/CategoryRepositoryTest.php b/tests/Unit/Domain/Category/CategoryRepositoryTest.php index 5a27a1d..a26d364 100644 --- a/tests/Unit/Domain/Category/CategoryRepositoryTest.php +++ b/tests/Unit/Domain/Category/CategoryRepositoryTest.php @@ -175,14 +175,19 @@ class CategoryRepositoryTest extends TestCase $mockDb = $this->createMock(\medoo::class); $mockDb->method('count')->willReturn(0); - $mockDb->expects($this->once()) + $deleteCalls = []; + $mockDb->expects($this->exactly(2)) ->method('delete') - ->with('pp_shop_categories', ['id' => 8]) - ->willReturn(true); + ->willReturnCallback(function ($table, $where) use (&$deleteCalls) { + $deleteCalls[] = ['table' => $table, 'where' => $where]; + return true; + }); $repository = new CategoryRepository($mockDb); $this->assertTrue($repository->categoryDelete(8)); + $this->assertSame('pp_shop_categories', $deleteCalls[0]['table']); + $this->assertSame('pp_routes', $deleteCalls[1]['table']); } public function testCategoryTitleReturnsEmptyWhenNotFound(): void