Files
shopPRO/autoload/Domain/Pages/PagesRepository.php
Jacek Pyziak d83d0ecdea 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 <noreply@anthropic.com>
2026-02-27 22:06:33 +01:00

790 lines
23 KiB
PHP

<?php
namespace Domain\Pages;
class PagesRepository
{
/**
* @var array<int, string>
*/
private const PAGE_TYPES = [
0 => 'pelne artykuly',
1 => 'wprowadzenia',
2 => 'miniaturki',
3 => 'link',
4 => 'kontakt',
5 => 'kategoria sklepu',
];
/**
* @var array<int, string>
*/
private const SORT_TYPES = [
0 => 'data dodania - najstarsze na poczatku',
1 => 'data dodania - najnowsze na poczatku',
2 => 'data modyfikacji - rosnaco',
3 => 'data modyfikacji - malejaco',
4 => 'reczne',
5 => 'alfabetycznie - A - Z',
6 => 'alfabetycznie - Z - A',
];
private $db;
public function __construct($db)
{
$this->db = $db;
}
/**
* @return array<int, string>
*/
public function pageTypes(): array
{
return self::PAGE_TYPES;
}
/**
* @return array<int, string>
*/
public function sortTypes(): array
{
return self::SORT_TYPES;
}
/**
* @return array<int, array<string, mixed>>
*/
public function menusList(): array
{
$rows = $this->db->select('pp_menus', '*', ['ORDER' => ['id' => 'ASC']]);
return is_array($rows) ? $rows : [];
}
/**
* @return array<int, array<string, mixed>>
*/
public function menusWithPages(): array
{
$menus = $this->menusList();
foreach ($menus as $index => $menu) {
$menuId = (int)($menu['id'] ?? 0);
$menus[$index]['pages'] = $this->menuPages($menuId);
}
return $menus;
}
/**
* @return array<int, array<string, mixed>>
*/
public function menuPages(int $menuId, ?int $parentId = null): array
{
if ($menuId <= 0) {
return [];
}
$rows = $this->db->select('pp_pages', ['id', 'menu_id', 'status', 'parent_id', 'start'], [
'AND' => [
'menu_id' => $menuId,
'parent_id' => $parentId,
],
'ORDER' => ['o' => 'ASC'],
]);
if (!is_array($rows)) {
return [];
}
$pages = [];
foreach ($rows as $row) {
$pageId = (int)($row['id'] ?? 0);
if ($pageId <= 0) {
continue;
}
$row['title'] = $this->pageTitle($pageId);
$row['languages'] = $this->pageLanguages($pageId);
$row['subpages'] = $this->menuPages($menuId, $pageId);
$pages[] = $row;
}
return $pages;
}
public function menuDelete(int $menuId): bool
{
if ($menuId <= 0) {
return false;
}
if ((int)$this->db->count('pp_pages', ['menu_id' => $menuId]) > 0) {
return false;
}
return (bool)$this->db->delete('pp_menus', ['id' => $menuId]);
}
public function pageDelete(int $pageId): bool
{
if ($pageId <= 0) {
return false;
}
if ((int)$this->db->count('pp_pages', ['parent_id' => $pageId]) > 0) {
return false;
}
$deleted = (bool)$this->db->delete('pp_pages', ['id' => $pageId]);
if ($deleted) {
$this->db->delete('pp_routes', ['page_id' => $pageId]);
}
return $deleted;
}
/**
* @return array<string, mixed>
*/
public function menuDetails(int $menuId): array
{
if ($menuId <= 0) {
return [
'id' => 0,
'name' => '',
'status' => 1,
];
}
$menu = $this->db->get('pp_menus', '*', ['id' => $menuId]);
if (!is_array($menu)) {
return [
'id' => 0,
'name' => '',
'status' => 1,
];
}
return $menu;
}
public function menuSave(int $menuId, string $name, $status): bool
{
$statusValue = $this->toSwitchValue($status);
if ($menuId <= 0) {
$result = $this->db->insert('pp_menus', [
'name' => $name,
'status' => $statusValue,
]);
if ($result) {
\Shared\Helpers\Helpers::delete_dir('../temp/');
}
return (bool)$result;
}
$this->db->update('pp_menus', [
'name' => $name,
'status' => $statusValue,
], [
'id' => $menuId,
]);
\Shared\Helpers\Helpers::delete_dir('../temp/');
return true;
}
/**
* @return array<string, mixed>
*/
public function pageDetails(int $pageId): array
{
if ($pageId <= 0) {
return $this->defaultPage();
}
$page = $this->db->get('pp_pages', '*', ['id' => $pageId]);
if (!is_array($page)) {
return $this->defaultPage();
}
$translations = $this->db->select('pp_pages_langs', '*', ['page_id' => $pageId]);
if (is_array($translations)) {
foreach ($translations as $row) {
$langId = (string)($row['lang_id'] ?? '');
if ($langId !== '') {
$page['languages'][$langId] = $row;
}
}
}
$page['layout_id'] = (int)$this->db->get('pp_layouts_pages', 'layout_id', ['page_id' => $pageId]);
return $page;
}
/**
* @return array<int, array<string, mixed>>
*/
public function pageArticles(int $pageId): array
{
if ($pageId <= 0) {
return [];
}
$sql = '
SELECT
ap.article_id,
ap.o,
a.status,
(
SELECT title
FROM pp_articles_langs AS pal
JOIN pp_langs AS pl ON pal.lang_id = pl.id
WHERE pal.article_id = ap.article_id
AND pal.title != ""
ORDER BY pl.o ASC
LIMIT 1
) AS title
FROM pp_articles_pages AS ap
JOIN pp_articles AS a ON a.id = ap.article_id
WHERE ap.page_id = :page_id
AND a.status != -1
ORDER BY ap.o ASC
';
$stmt = $this->db->query($sql, [':page_id' => $pageId]);
$rows = $stmt ? $stmt->fetchAll() : [];
return is_array($rows) ? $rows : [];
}
public function saveArticlesOrder(int $pageId, $articles): bool
{
if ($pageId <= 0) {
return false;
}
if (!is_array($articles)) {
return true;
}
$this->db->update('pp_articles_pages', ['o' => 0], ['page_id' => $pageId]);
$position = 0;
foreach ($articles as $item) {
$articleId = (int)($item['item_id'] ?? 0);
if ($articleId <= 0) {
continue;
}
$position++;
$this->db->update('pp_articles_pages', ['o' => $position], [
'AND' => [
'page_id' => $pageId,
'article_id' => $articleId,
],
]);
}
\Shared\Helpers\Helpers::delete_dir('../temp/');
return true;
}
public function savePagesOrder(int $menuId, $pages): bool
{
if ($menuId <= 0) {
return false;
}
if (!is_array($pages)) {
return true;
}
$this->db->update('pp_pages', ['o' => 0], ['menu_id' => $menuId]);
$position = 0;
foreach ($pages as $item) {
$itemId = (int)($item['item_id'] ?? 0);
$depth = (int)($item['depth'] ?? 0);
if ($itemId <= 0 || $depth <= 1) {
continue;
}
$parentId = (int)($item['parent_id'] ?? 0);
if ($depth === 2) {
$parentId = null;
}
$position++;
$this->db->update('pp_pages', [
'o' => $position,
'parent_id' => $parentId,
], [
'id' => $itemId,
]);
}
\Shared\Helpers\Helpers::delete_dir('../temp/');
return true;
}
public function pageSave(array $data): ?int
{
$pageId = (int)($data['id'] ?? 0);
$menuId = (int)($data['menu_id'] ?? 0);
$parentId = $this->normalizeNullableInt($data['parent_id'] ?? null);
$pageType = (int)($data['page_type'] ?? 0);
$sortType = (int)($data['sort_type'] ?? 0);
$layoutId = (int)($data['layout_id'] ?? 0);
$articlesLimit = (int)($data['articles_limit'] ?? 0);
$showTitle = $this->toSwitchValue($data['show_title'] ?? 0);
$status = $this->toSwitchValue($data['status'] ?? 0);
$start = $this->toSwitchValue($data['start'] ?? 0);
$categoryId = $this->normalizeNullableInt($data['category_id'] ?? null);
if ($pageType !== 5) {
$categoryId = null;
}
if ($pageId <= 0) {
$order = $this->maxPageOrder() + 1;
$result = $this->db->insert('pp_pages', [
'menu_id' => $menuId,
'page_type' => $pageType,
'sort_type' => $sortType,
'articles_limit' => $articlesLimit,
'show_title' => $showTitle,
'status' => $status,
'o' => $order,
'parent_id' => $parentId,
'start' => $start,
'category_id' => $categoryId,
]);
if (!$result) {
return null;
}
$pageId = (int)$this->db->id();
if ($pageId <= 0) {
return null;
}
} else {
$this->db->update('pp_pages', [
'menu_id' => $menuId,
'page_type' => $pageType,
'sort_type' => $sortType,
'articles_limit' => $articlesLimit,
'show_title' => $showTitle,
'status' => $status,
'parent_id' => $parentId,
'start' => $start,
'category_id' => $categoryId,
], [
'id' => $pageId,
]);
}
if ($start === 1) {
$this->db->update('pp_pages', ['start' => 0], ['id[!]' => $pageId]);
$this->db->update('pp_pages', ['start' => 1], ['id' => $pageId]);
}
$this->db->delete('pp_layouts_pages', ['page_id' => $pageId]);
if ($layoutId > 0) {
$this->db->insert('pp_layouts_pages', [
'layout_id' => $layoutId,
'page_id' => $pageId,
]);
}
$this->saveTranslations($pageId, $pageType, $data);
$this->updateSubpagesMenuId($pageId, $menuId);
\Shared\Helpers\Helpers::htacces();
\Shared\Helpers\Helpers::delete_dir('../temp/');
return $pageId;
}
public function generateSeoLink(string $title, int $pageId = 0, int $articleId = 0, int $categoryId = 0): string
{
$base = trim((string)\Shared\Helpers\Helpers::seo($title));
if ($base === '') {
return '';
}
$candidate = $base;
$suffix = 0;
while ($this->isSeoLinkUsed('pp_pages_langs', 'page_id', $candidate, $pageId)
|| $this->isSeoLinkUsed('pp_articles_langs', 'article_id', $candidate, $articleId)
|| $this->isSeoLinkUsed('pp_shop_categories_langs', 'category_id', $candidate, $categoryId)) {
$suffix++;
$candidate = $base . '-' . $suffix;
}
return $candidate;
}
public function pageUrlPreview(int $pageId, string $langId, string $title, string $seoLink, string $defaultLanguageId): string
{
$url = trim($seoLink) !== ''
? '/' . ltrim($seoLink, '/')
: '/s-' . $pageId . '-' . \Shared\Helpers\Helpers::seo($title);
if ($langId !== '' && $langId !== $defaultLanguageId && $url !== '#') {
$url = '/' . $langId . $url;
}
return $url;
}
public function toggleCookieValue(string $cookieName, int $itemId): void
{
if ($cookieName === '' || $itemId <= 0) {
return;
}
$state = [];
if (!empty($_COOKIE[$cookieName])) {
$decoded = @unserialize((string)$_COOKIE[$cookieName], ['allowed_classes' => false]);
if (is_array($decoded)) {
$state = $decoded;
}
}
$state[$itemId] = empty($state[$itemId]) ? 1 : 0;
setcookie($cookieName, serialize($state), time() + 3600 * 24 * 365);
}
public function pageTitle(int $pageId): string
{
if ($pageId <= 0) {
return '';
}
$rows = $this->db->select('pp_pages_langs', [
'[><]pp_langs' => ['lang_id' => 'id'],
], 'title', [
'AND' => [
'page_id' => $pageId,
'title[!]' => '',
],
'ORDER' => ['o' => 'ASC'],
'LIMIT' => 1,
]);
if (!is_array($rows) || !isset($rows[0])) {
return '';
}
return (string)$rows[0];
}
/**
* @return array<int, array<string, mixed>>
*/
public function pageLanguages(int $pageId): array
{
if ($pageId <= 0) {
return [];
}
$rows = $this->db->select('pp_pages_langs', '*', [
'AND' => [
'page_id' => $pageId,
'title[!]' => null,
],
]);
return is_array($rows) ? $rows : [];
}
/**
* @return array<string, mixed>
*/
private function defaultPage(): array
{
return [
'id' => 0,
'menu_id' => 0,
'page_type' => 0,
'sort_type' => 0,
'articles_limit' => 2,
'show_title' => 1,
'status' => 1,
'start' => 0,
'parent_id' => null,
'category_id' => null,
'layout_id' => 0,
'languages' => [],
];
}
private function saveTranslations(int $pageId, int $pageType, array $data): void
{
$titles = is_array($data['title'] ?? null) ? $data['title'] : [];
$seoLinks = is_array($data['seo_link'] ?? null) ? $data['seo_link'] : [];
$metaTitles = is_array($data['meta_title'] ?? null) ? $data['meta_title'] : [];
$metaDescriptions = is_array($data['meta_description'] ?? null) ? $data['meta_description'] : [];
$metaKeywords = is_array($data['meta_keywords'] ?? null) ? $data['meta_keywords'] : [];
$noindexValues = is_array($data['noindex'] ?? null) ? $data['noindex'] : [];
$pageTitles = is_array($data['page_title'] ?? null) ? $data['page_title'] : [];
$links = is_array($data['link'] ?? null) ? $data['link'] : [];
$canonicals = is_array($data['canonical'] ?? null) ? $data['canonical'] : [];
foreach ($titles as $langId => $title) {
$langId = (string)$langId;
if ($langId === '') {
continue;
}
$row = [
'lang_id' => $langId,
'title' => $this->nullIfEmpty($title),
'meta_description' => $this->nullIfEmpty($metaDescriptions[$langId] ?? null),
'meta_keywords' => $this->nullIfEmpty($metaKeywords[$langId] ?? null),
'meta_title' => $this->nullIfEmpty($metaTitles[$langId] ?? null),
'seo_link' => $this->nullIfEmpty(\Shared\Helpers\Helpers::seo((string)($seoLinks[$langId] ?? ''))),
'noindex' => (int)($noindexValues[$langId] ?? 0),
'page_title' => $this->nullIfEmpty($pageTitles[$langId] ?? null),
'link' => $pageType === 3 ? $this->nullIfEmpty($links[$langId] ?? null) : null,
'canonical' => $this->nullIfEmpty($canonicals[$langId] ?? null),
];
$translationId = (int)$this->db->get('pp_pages_langs', 'id', [
'AND' => [
'page_id' => $pageId,
'lang_id' => $langId,
],
]);
if ($translationId > 0) {
$this->db->update('pp_pages_langs', $row, ['id' => $translationId]);
} else {
$row['page_id'] = $pageId;
$this->db->insert('pp_pages_langs', $row);
}
}
}
private function updateSubpagesMenuId(int $parentId, int $menuId): void
{
if ($parentId <= 0 || $menuId <= 0) {
return;
}
$this->db->update('pp_pages', ['menu_id' => $menuId], ['parent_id' => $parentId]);
$children = $this->db->select('pp_pages', ['id'], ['parent_id' => $parentId]);
if (!is_array($children)) {
return;
}
foreach ($children as $row) {
$childId = (int)($row['id'] ?? 0);
if ($childId > 0) {
$this->updateSubpagesMenuId($childId, $menuId);
}
}
}
private function isSeoLinkUsed(string $table, string $idColumn, string $seoLink, int $exceptId): bool
{
$where = [
'seo_link' => $seoLink,
];
if ($exceptId > 0) {
$where[$idColumn . '[!]'] = $exceptId;
}
return (int)$this->db->count($table, ['AND' => $where]) > 0;
}
private function maxPageOrder(): int
{
$max = $this->db->max('pp_pages', 'o');
return $max ? (int)$max : 0;
}
private function toSwitchValue($value): int
{
if ($value === 'on' || $value === '1' || $value === 1 || $value === true) {
return 1;
}
return 0;
}
private function normalizeNullableInt($value): ?int
{
if ($value === null || $value === '' || (int)$value === 0) {
return null;
}
return (int)$value;
}
private function nullIfEmpty($value): ?string
{
$value = trim((string)$value);
return $value === '' ? null : $value;
}
// ── Frontend methods ──────────────────────────────────────────
public function frontPageDetails($id = '', $langId = ''): ?array
{
$langId = (string)$langId;
if (!$id) {
$id = $this->frontMainPageId();
}
$cacheHandler = new \Shared\Cache\CacheHandler();
$cacheKey = "PagesRepository::frontPageDetails:$id:$langId";
$objectData = $cacheHandler->get($cacheKey);
if ($objectData) {
$cached = @unserialize($objectData);
if (is_array($cached)) {
return $cached;
}
$cacheHandler->delete($cacheKey);
}
$page = $this->db->get('pp_pages', '*', ['id' => (int)$id]);
if (!is_array($page)) {
return null;
}
$page['language'] = $this->db->get('pp_pages_langs', '*', ['AND' => ['page_id' => (int)$id, 'lang_id' => $langId]]);
$cacheHandler->set($cacheKey, $page);
return $page;
}
public function frontPageSort(int $pageId)
{
$cacheHandler = new \Shared\Cache\CacheHandler();
$cacheKey = "PagesRepository::frontPageSort:$pageId";
$objectData = $cacheHandler->get($cacheKey);
if ($objectData) {
$cached = @unserialize($objectData);
if ($cached !== false) {
return $cached;
}
$cacheHandler->delete($cacheKey);
}
$sort = $this->db->get('pp_pages', 'sort_type', ['id' => $pageId]);
$cacheHandler->set($cacheKey, $sort);
return $sort;
}
public function frontMainPageId()
{
$cacheHandler = new \Shared\Cache\CacheHandler();
$cacheKey = 'PagesRepository::frontMainPageId';
$objectData = $cacheHandler->get($cacheKey);
if ($objectData) {
$cached = @unserialize($objectData);
if ($cached) {
return $cached;
}
$cacheHandler->delete($cacheKey);
}
$id = $this->db->get('pp_pages', 'id', ['AND' => ['status' => 1, 'start' => 1]]);
if (!$id) {
$id = $this->db->get('pp_pages', 'id', ['status' => 1, 'ORDER' => ['menu_id' => 'ASC', 'o' => 'ASC'], 'LIMIT' => 1]);
}
$cacheHandler->set($cacheKey, $id);
return $id;
}
public function frontLangUrl(int $pageId, string $langId): string
{
$page = $this->frontPageDetails($pageId, $langId);
if (!is_array($page) || !is_array($page['language'] ?? null)) {
return '/';
}
$seoLink = $page['language']['seo_link'] ?? '';
$title = $page['language']['title'] ?? '';
$url = $seoLink ? '/' . $seoLink : '/s-' . $page['id'] . '-' . \Shared\Helpers\Helpers::seo($title);
$defaultLang = (new \Domain\Languages\LanguagesRepository($this->db))->defaultLanguage();
if ($langId !== $defaultLang && $url !== '#') {
$url = '/' . $langId . $url;
}
return $url;
}
public function frontMenuDetails(int $menuId, string $langId): ?array
{
$cacheHandler = new \Shared\Cache\CacheHandler();
$cacheKey = "PagesRepository::frontMenuDetails:$menuId:$langId";
$objectData = $cacheHandler->get($cacheKey);
if ($objectData) {
$cached = @unserialize($objectData);
if (is_array($cached)) {
return $cached;
}
$cacheHandler->delete($cacheKey);
}
$menu = $this->db->get('pp_menus', '*', ['id' => (int)$menuId]);
if (!is_array($menu)) {
return null;
}
$menu['pages'] = $this->frontMenuPages($menuId, $langId);
$cacheHandler->set($cacheKey, $menu);
return $menu;
}
public function frontMenuPages(int $menuId, string $langId, $parentId = null): ?array
{
$cacheHandler = new \Shared\Cache\CacheHandler();
$cacheKey = "PagesRepository::frontMenuPages:$menuId:$langId:$parentId";
$objectData = $cacheHandler->get($cacheKey);
if ($objectData) {
$cached = @unserialize($objectData);
if (is_array($cached)) {
return $cached;
}
$cacheHandler->delete($cacheKey);
}
$results = $this->db->select('pp_pages', ['id'], [
'AND' => ['status' => 1, 'menu_id' => (int)$menuId, 'parent_id' => $parentId],
'ORDER' => ['o' => 'ASC'],
]);
$pages = [];
if (is_array($results)) {
foreach ($results as $row) {
$page = $this->frontPageDetails($row['id'], $langId);
if (is_array($page)) {
$page['pages'] = $this->frontMenuPages($menuId, $langId, $row['id']);
$pages[] = $page;
}
}
}
$cacheHandler->set($cacheKey, $pages);
return $pages;
}
}