Files
cmsPRO/PLAN_REFAKTORINGU.md
2026-02-22 21:59:33 +01:00

971 lines
33 KiB
Markdown

# PLAN REFAKTORINGU cmsPRO
## Cel
Przenieść architekturę z shopPRO do cmsPRO:
- **Domain-Driven Design** z warstwą `\Domain\` (repozytoria z DI)
- **Kontrolery z Dependency Injection** (zamiast statycznych `controls` + `factory`)
- **Nowy system aktualizacji** z manifestami JSON, checksumami SHA256, backupami
- **Shared utilities** w namespace `\Shared\`
- **Testy PHPUnit**
- **Automatyczny build paczek** (`build-update.ps1`)
---
## STAN OBECNY cmsPRO vs DOCELOWY
### Obecna architektura (legacy)
```
autoload/
├── admin/
│ ├── class.Site.php ← Router (statyczne metody, global $mdb)
│ ├── controls/ ← 16 kontrolerów (statyczne, global $mdb)
│ ├── factory/ ← 15 klas logiki biznesowej (statyczne, global $mdb)
│ └── view/ ← 17 klas widoków (statyczne)
├── front/
│ ├── controls/ ← 4 kontrolery
│ ├── factory/ ← 13 klas logiki
│ └── view/ ← 8 klas widoków
├── class.S.php ← Mega-utility (sesja, cache, email, SEO, token, DOM...)
├── class.Tpl.php ← Template engine
├── class.Cache.php ← Cache plikowy (gzdeflate + MD5)
├── class.Html.php ← Komponenty formularzy HTML
├── class.Image.php ← ImageManipulator
├── class.Article.php ← Model artykułu z ArrayAccess
├── class.Scontainer.php ← Kontenery statyczne z ArrayAccess
├── class.Page.php ← Model strony
├── class.Mobile_Detect.php ← Detekcja mobilnych
└── class.geoplugin.php ← GeoIP
```
### Architektura docelowa (wzór: shopPRO)
```
autoload/
├── Domain/ ← NOWE: warstwa domenowa
│ ├── Article/
│ │ └── ArticleRepository.php
│ ├── Banner/
│ │ └── BannerRepository.php
│ ├── Languages/
│ │ └── LanguagesRepository.php
│ ├── Layouts/
│ │ └── LayoutsRepository.php
│ ├── Newsletter/
│ │ ├── NewsletterRepository.php
│ │ └── NewsletterPreviewRenderer.php
│ ├── Pages/
│ │ └── PagesRepository.php
│ ├── Scontainers/
│ │ └── ScontainersRepository.php
│ ├── Settings/
│ │ └── SettingsRepository.php
│ ├── Update/
│ │ └── UpdateRepository.php
│ ├── User/
│ │ └── UserRepository.php
│ ├── Author/
│ │ └── AuthorRepository.php
│ ├── SeoAdditional/
│ │ └── SeoAdditionalRepository.php
│ ├── Cache/
│ │ └── CacheRepository.php
│ ├── Email/
│ │ └── EmailRepository.php
│ └── Backup/
│ └── BackupRepository.php
├── Shared/ ← NOWE: wspólne utility
│ ├── Helpers/
│ │ └── Helpers.php ← Refaktor z class.S.php
│ ├── Tpl/
│ │ └── Tpl.php ← Refaktor z class.Tpl.php
│ ├── Cache/
│ │ └── CacheHandler.php ← Refaktor z class.Cache.php
│ ├── Html/
│ │ └── Html.php ← Refaktor z class.Html.php
│ ├── Image/
│ │ └── ImageManipulator.php ← Refaktor z class.Image.php
│ └── Email/
│ └── Email.php ← PHPMailer wrapper
├── admin/
│ ├── App.php ← NOWY: Router + DI container (zamienia Site.php)
│ ├── Controllers/ ← NOWE: kontrolery z DI
│ │ ├── ArticlesController.php
│ │ ├── ArticlesArchiveController.php
│ │ ├── AuthorsController.php
│ │ ├── BackupsController.php
│ │ ├── BannerController.php
│ │ ├── EmailsController.php
│ │ ├── FilemanagerController.php
│ │ ├── LanguagesController.php
│ │ ├── LayoutsController.php
│ │ ├── NewsletterController.php
│ │ ├── PagesController.php
│ │ ├── ScontainersController.php
│ │ ├── SeoAdditionalController.php
│ │ ├── SettingsController.php
│ │ ├── UpdateController.php
│ │ └── UsersController.php
│ ├── controls/ ← USUNĄĆ (po migracji)
│ ├── factory/ ← USUNĄĆ (po migracji)
│ └── view/ ← USUNĄĆ (po migracji — logic do Controllers)
├── front/
│ ├── App.php ← NOWY: Router + DI container
│ ├── LayoutEngine.php ← NOWY: refaktor z front\view\Site::show()
│ ├── Controllers/ ← NOWE: kontrolery z DI
│ │ ├── NewsletterController.php
│ │ └── SearchController.php (+ ewentualne inne)
│ ├── Views/ ← NOWE: statyczne klasy renderujące (z front\view\)
│ │ ├── Articles.php
│ │ ├── Banners.php
│ │ ├── Languages.php
│ │ ├── Menu.php
│ │ ├── Newsletter.php
│ │ ├── Scontainers.php
│ │ └── Search.php
│ ├── controls/ ← USUNĄĆ (po migracji)
│ ├── factory/ ← USUNĄĆ (po migracji)
│ └── view/ ← USUNĄĆ (po migracji — do Views/ + LayoutEngine)
└── (stare class.*.php) ← USUNĄĆ (po migracji do Shared/)
```
---
## FAZY REFAKTORINGU
Każda faza kończy się działającym systemem. Stary i nowy kod koegzystują aż do pełnej migracji.
---
## FAZA 0: Przygotowanie infrastruktury
### 0.1 Aktualizacja autoloadera
**Obecny autoloader** (index.php, admin/index.php) szuka tylko `class.{Name}.php`:
```php
$f = 'autoload/' . implode('/', $q) . '/class.' . $c . '.php';
```
**Docelowy** — musi obsługiwać OBE konwencje (fallback PSR-4):
```php
function __autoload_my_classes($classname) {
$q = explode('\\', $classname);
$c = array_pop($q);
// 1. Legacy: class.ClassName.php
$f = 'autoload/' . implode('/', $q) . '/class.' . $c . '.php';
if (file_exists($f)) { require_once($f); return; }
// 2. PSR-4: ClassName.php
$f = 'autoload/' . implode('/', $q) . '/' . $c . '.php';
if (file_exists($f)) require_once($f);
}
```
**Pliki do zmiany:**
- `index.php` (linia ~3-11)
- `admin/index.php` (linia ~15-22, ścieżka `../autoload/`)
- `ajax.php` (jeśli ma autoloader)
- `admin/ajax.php`
- `api.php`
- `cron.php`
### 0.2 Utworzenie katalogów
```
mkdir autoload/Domain
mkdir autoload/Shared
mkdir autoload/Shared/Helpers
mkdir autoload/Shared/Tpl
mkdir autoload/Shared/Cache
mkdir autoload/Shared/Html
mkdir autoload/Shared/Image
mkdir autoload/Shared/Email
mkdir autoload/admin/Controllers
mkdir autoload/front/Controllers
mkdir autoload/front/Views
mkdir tests
mkdir tests/Unit
mkdir tests/Unit/Domain
mkdir tests/stubs
mkdir migrations
mkdir docs
mkdir backups
```
### 0.3 Konfiguracja PHPUnit
Skopiować z shopPRO:
- `phpunit.xml`
- `phpunit.phar`
- `test.ps1`
- `tests/bootstrap.php`
- `composer.json` (sekcja `require-dev`)
### 0.4 Git tagging — ustalenie punktu startowego
```bash
git tag v1.689 # bieżąca wersja cmsPRO
git push origin v1.689
```
Od następnej wersji (1.690) zaczyna się nowy system aktualizacji.
### 0.5 Utworzenie CLAUDE.md i AGENTS.md
Skopiować strukturę z shopPRO, dostosować do cmsPRO (brak modułu sklepowego, inne moduły).
---
## FAZA 1: Shared Utilities (przeniesienie klas globalnych)
Kolejność: Helpers → Tpl → Cache → Html → Image → Email.
Po każdym kroku starą klasę zostawiamy jako wrapper (deleguje do nowej), żeby nic się nie zepsuło.
### 1.1 `\Shared\Helpers\Helpers` (z `class.S.php`)
**Plik:** `autoload/Shared/Helpers/Helpers.php`
Skopiować WSZYSTKIE metody z `\S` do `\Shared\Helpers\Helpers`:
- `get()`, `get_session()`, `set_session()`, `delete_session()`
- `alert()`, `set_message()`, `lang()`
- `send_email()`, `email_check()`
- `seo()`, `noPL()`
- `cache_write()`, `cache_read()`, `cache_file_url()`
- `htacces()`
- `delete_cache()`, `delete_dir()`, `is_empty_dir()`
- `date_diff()`, `months()`, `months_short()`
- `get_token()`, `is_token_valid()`
- DOM manipulation: `suSetHtmlById()`, `suAddHtmlById()`, itd.
- `get_version()`, `get_new_version()`
- `Pre()`, `json_to_array()`, `is_bot()`, `is_mobile()`
- `generate_webp_image()`
**Stary `class.S.php`** — zamienić na wrapper:
```php
class S {
public static function __callStatic($name, $args) {
return call_user_func_array(['\Shared\Helpers\Helpers', $name], $args);
}
// LUB: jawne delegacje dla każdej metody
}
```
**UWAGA:** `\S::` jest używane WSZĘDZIE w cmsPRO (controls, factory, view, templates, index.php). Delegacja przez wrapper pozwala na stopniową migrację referencji.
### 1.2 `\Shared\Tpl\Tpl` (z `class.Tpl.php`)
**Plik:** `autoload/Shared/Tpl/Tpl.php`
**Namespace:** `\Shared\Tpl\`
Stary `class.Tpl.php` → wrapper delegujący do `\Shared\Tpl\Tpl`.
### 1.3 `\Shared\Cache\CacheHandler` (z `class.Cache.php`)
**Plik:** `autoload/Shared/Cache/CacheHandler.php`
**Namespace:** `\Shared\Cache\`
cmsPRO używa cache plikowego (gzdeflate + MD5), NIE Redis.
Zachować tę samą implementację, tylko przenieść do nowego namespace.
Stary `class.Cache.php` → wrapper.
### 1.4 `\Shared\Html\Html` (z `class.Html.php`)
**Plik:** `autoload/Shared/Html/Html.php`
Stary `class.Html.php` → wrapper.
### 1.5 `\Shared\Image\ImageManipulator` (z `class.Image.php`)
**Plik:** `autoload/Shared/Image/ImageManipulator.php`
Stary `class.Image.php` (klasa `ImageManipulator`) → wrapper lub alias.
### 1.6 `\Shared\Email\Email`
**Plik:** `autoload/Shared/Email/Email.php`
Nowa klasa opakowująca PHPMailer (wzór: shopPRO `Shared\Email\Email`).
### 1.7 Usunięcie starych klas globalnych
**PO PEŁNEJ MIGRACJI** wszystkich referencji (to będzie trwać przez kolejne fazy):
- `class.Article.php` → logika do `Domain\Article\ArticleRepository`
- `class.Scontainer.php` → logika do `Domain\Scontainers\ScontainersRepository`
- `class.Page.php` → logika do `Domain\Pages\PagesRepository`
`class.Mobile_Detect.php` i `class.geoplugin.php` zostawić (biblioteki zewnętrzne).
---
## FAZA 2: Warstwa Domain (repozytoria)
Dla KAŻDEGO modułu: wyciągnąć logikę z `admin\factory\*` i `front\factory\*` do wspólnego repozytorium w `Domain\`.
### Wzorzec repozytorium (kopiowany z shopPRO)
```php
<?php
namespace Domain\Article;
class ArticleRepository
{
private $db;
public function __construct($db)
{
$this->db = $db;
}
public function find(int $id): ?array
{
return $this->db->get('pp_articles', '*', ['id' => $id]);
}
// ... reszta metod
}
```
### Lista modułów do migracji (16 modułów)
| # | Moduł | Źródło (factory) | Cel (Domain) | Priorytet |
|---|-------|-------------------|--------------|-----------|
| 1 | **Settings** | `admin\factory\Settings` + `front\factory\Settings` | `Domain\Settings\SettingsRepository` | WYSOKI — używany wszędzie |
| 2 | **Languages** | `admin\factory\Languages` + `front\factory\Languages` | `Domain\Languages\LanguagesRepository` | WYSOKI — używany w bootstrap |
| 3 | **Users** | `admin\factory\Users` | `Domain\User\UserRepository` | WYSOKI — login, 2FA, uprawnienia |
| 4 | **Pages** | `admin\factory\Pages` + `front\factory\Pages` | `Domain\Pages\PagesRepository` | WYSOKI — routing frontu |
| 5 | **Articles** | `admin\factory\Articles` + `front\factory\Articles` + `class.Article.php` | `Domain\Article\ArticleRepository` | WYSOKI — główna funkcjonalność |
| 6 | **Layouts** | `admin\factory\Layouts` + `front\factory\Layouts` | `Domain\Layouts\LayoutsRepository` | WYSOKI — layout engine |
| 7 | **Scontainers** | `admin\factory\Scontainers` + `front\factory\Scontainers` + `class.Scontainer.php` | `Domain\Scontainers\ScontainersRepository` | ŚREDNI |
| 8 | **Banners** | `admin\factory\Banners` + `front\factory\Banners` | `Domain\Banner\BannerRepository` | ŚREDNI |
| 9 | **Newsletter** | `admin\factory\Newsletter` + `front\factory\Newsletter` | `Domain\Newsletter\NewsletterRepository` | ŚREDNI |
| 10 | **Authors** | `admin\factory\Authors` + `front\factory\Authors` | `Domain\Author\AuthorRepository` | NISKI |
| 11 | **Emails** | `admin\factory\Emails` | `Domain\Email\EmailRepository` | NISKI |
| 12 | **Backups** | `admin\factory\Backups` | `Domain\Backup\BackupRepository` | NISKI |
| 13 | **SeoAdditional** | `admin\factory\SeoAdditional` + `front\factory\SeoAdditional` | `Domain\SeoAdditional\SeoAdditionalRepository` | NISKI |
| 14 | **ArticlesArchive** | `admin\factory\ArticlesArchive` | (część ArticleRepository) | NISKI |
| 15 | **Menu** | `front\factory\Menu` | `Domain\Menu\MenuRepository` | ŚREDNI |
| 16 | **Update** | `admin\factory\Update` | `Domain\Update\UpdateRepository` | WYSOKI — nowy system |
### Proces migracji jednego modułu (przykład: Settings)
1. **Utworzyć** `autoload/Domain/Settings/SettingsRepository.php`
2. **Przenieść metody** z `admin\factory\Settings` i `front\factory\Settings`:
- `settings_details()``allSettings()`
- Zamienić `global $mdb``$this->db`
3. **Napisać testy** w `tests/Unit/Domain/Settings/SettingsRepositoryTest.php`
4. **Zaktualizować** wywołujących (controls, view, index.php):
- `\front\factory\Settings::settings_details()``$settingsRepo->allSettings()`
5. **Stary factory** — zostawić tymczasowo jako wrapper:
```php
namespace front\factory;
class Settings {
public static function settings_details() {
global $mdb;
return (new \Domain\Settings\SettingsRepository($mdb))->allSettings();
}
}
```
6. **Po migracji wszystkich wywołujących** — usunąć stary factory.
### Specjalne przypadki
**`class.Article.php`** — to model z ArrayAccess, nie zwykły factory:
- Konstruktor ładuje artykuł z bazy + images + files + pages + tags + params
- Szablony frontu używają `$article->field` (obiekt)
- **Strategia:** Przenieść logikę do `ArticleRepository`, zwracać tablice. Szablony frontu zostawić na później (użyć wrappera ArrayAccess tymczasowo).
**`class.Scontainer.php`** — analogicznie, implementuje ArrayAccess.
---
## FAZA 3: Admin Controllers z DI
### 3.1 Nowy router: `admin\App`
**Plik:** `autoload/admin/App.php`
Wzorowany 1:1 na shopPRO `admin\App`. Kluczowe elementy:
```php
<?php
namespace admin;
class App
{
const APP_SECRET_KEY = 'c3cb2537d25c0efc9e573d059d79c3b8';
private static $newControllers = [];
public static function special_actions() { /* z admin\Site */ }
public static function finalize_admin_login(...) { /* z admin\Site */ }
public static function render(): string
{
global $user;
if (\Shared\Helpers\Helpers::get('module') === 'user'
&& \Shared\Helpers\Helpers::get('action') === 'twofa') {
$controller = self::createController('Users');
return $controller->twofa();
}
if (!$user || !$user['admin']) {
$controller = self::createController('Users');
return $controller->login_form();
}
$tpl = new \Shared\Tpl\Tpl;
$tpl->content = self::route();
return $tpl->render('site/main-layout');
}
public static function route()
{
$_SESSION['admin'] = true;
$moduleName = '';
$parts = explode('_', (string)\Shared\Helpers\Helpers::get('module'));
foreach ($parts as $part)
$moduleName .= ucfirst($part);
$action = \Shared\Helpers\Helpers::get('action');
// Najpierw nowe kontrolery z DI
$controller = self::createController($moduleName);
if ($controller && method_exists($controller, $action))
return $controller->$action();
// Fallback do starych controls (okres przejściowy)
$class = '\admin\controls\\' . $moduleName;
if (class_exists($class) && method_exists(new $class, $action))
return call_user_func_array([$class, $action], []);
\Shared\Helpers\Helpers::alert('Nieprawidłowy adres url.');
return false;
}
private static function createController(string $moduleName)
{
$factories = self::getControllerFactories();
if (!isset($factories[$moduleName])) return null;
$factory = $factories[$moduleName];
return is_callable($factory) ? $factory() : null;
}
private static function getControllerFactories(): array
{
if (!empty(self::$newControllers))
return self::$newControllers;
self::$newControllers = [
// Dodawane stopniowo w miarę migracji modułów
];
return self::$newControllers;
}
public static function update()
{
global $mdb;
$repository = new \Domain\Update\UpdateRepository($mdb);
$repository->runPendingMigrations();
}
}
```
**Przełączenie:**
- W `admin/index.php`: zamienić `\admin\Site::special_actions()` → `\admin\App::special_actions()`
- Zamienić `\admin\view\Page::show()` → `\admin\App::render()`
- Dodać `\admin\App::update()` (migracje SQL)
Fallback do starych `\admin\controls\*` zapewnia, że system działa ZANIM wszystkie kontrolery zostaną zmigrowane.
### 3.2 Nowe kontrolery admin (po jednym na moduł)
**Wzorzec kontrolera** (kopiowany z shopPRO):
```php
<?php
namespace admin\Controllers;
use Domain\Article\ArticleRepository;
use Domain\Languages\LanguagesRepository;
class ArticlesController
{
private $articleRepo;
private $langRepo;
public function __construct(
ArticleRepository $articleRepo,
LanguagesRepository $langRepo
) {
$this->articleRepo = $articleRepo;
$this->langRepo = $langRepo;
}
public function list()
{
// logika z admin\controls\Articles::view_list()
// + renderowanie szablonu
}
public function edit()
{
// logika z admin\controls\Articles::article_edit()
}
public function save()
{
// logika z admin\controls\Articles::article_save()
}
}
```
### 3.3 Kolejność migracji kontrolerów admin
| # | Kontroler | Factory → Repository | Wiring w App |
|---|-----------|---------------------|--------------|
| 1 | `UsersController` | `Users` → `UserRepository` | `'Users' => fn() => new UsersController(new UserRepository($mdb))` |
| 2 | `SettingsController` | `Settings` → `SettingsRepository` | `'Settings' => fn() => ...` |
| 3 | `UpdateController` | `Update` → `UpdateRepository` | `'Update' => fn() => ...` |
| 4 | `LanguagesController` | `Languages` → `LanguagesRepository` | |
| 5 | `PagesController` | `Pages` → `PagesRepository` | |
| 6 | `ArticlesController` | `Articles` → `ArticleRepository` | |
| 7 | `ArticlesArchiveController` | `ArticlesArchive` | |
| 8 | `LayoutsController` | `Layouts` → `LayoutsRepository` | |
| 9 | `ScontainersController` | `Scontainers` → `ScontainersRepository` | |
| 10 | `BannerController` | `Banners` → `BannerRepository` | |
| 11 | `NewsletterController` | `Newsletter` → `NewsletterRepository` | |
| 12 | `AuthorsController` | `Authors` → `AuthorRepository` | |
| 13 | `EmailsController` | `Emails` → `EmailRepository` | |
| 14 | `BackupsController` | `Backups` → `BackupRepository` | |
| 15 | `SeoAdditionalController` | `SeoAdditional` → `SeoAdditionalRepository` | |
| 16 | `FilemanagerController` | (brak factory — sam w sobie) | |
### 3.4 Mapowanie akcji (stare → nowe nazwy metod)
Konwencja shopPRO: camelCase zamiast snake_case.
| Stara akcja (URL) | Nowa metoda |
|--------------------|-------------|
| `view_list` | `list()` |
| `article_edit` | `edit()` |
| `article_save` | `save()` |
| `article_delete` | `delete()` |
| `main_view` | `main_view()` (bez zmian, bo URL tego wymaga) |
**UWAGA:** URL w admin to `?module=articles&action=view_list`. Router konwertuje `_` na ucfirst, ale akcja jest przekazywana dosłownie. Trzeba zachować dokładne nazwy metod odpowiadające `action` w URL, LUB dodać mapowanie camelCase.
Rekomendacja: dodać konwersję w routerze (jak shopPRO front\App robi `lcfirst(implode('', array_map('ucfirst', explode('_', $action))))`) LUB zostawić snake_case w nazwach metod kontrolerów.
---
## FAZA 4: Frontend (App + Controllers + Views + LayoutEngine)
### 4.1 `front\App` — nowy router
**Plik:** `autoload/front/App.php`
Wzorowany na shopPRO. Kluczowe:
- `checkUrlParams()` — obsługa `?a=page`, `?a=change_language`
- `route()` — routing do artykułów + kontrolerów DI + fallback do starych controls
- `getControllerFactories()` — fabryki kontrolerów
- `pageTitle()`, `title()` — tytuły stron
### 4.2 `front\LayoutEngine` — silnik szablonów
**Plik:** `autoload/front/LayoutEngine.php`
Refaktor z `front\view\Site::show()`. Odpowiedzialny za:
- Ładowanie layoutu (aktywny dla strony)
- Zastępowanie tagów: `[MENU:id]`, `[KONTENER:id]`, `[AKTUALNOSCI:...]`, `[ZAWARTOSC]`, `[TITLE]`, `[LANG:key]`, itp.
- Kontakt (formularz)
- Dodatkowe kody (statystyki, cookies)
### 4.3 Frontend Views (statyczne)
Przenieść `front\view\*` → `front\Views\*` (nowy namespace):
| Stara klasa | Nowa klasa |
|-------------|------------|
| `\front\view\Articles` | `\front\Views\Articles` |
| `\front\view\Banners` | `\front\Views\Banners` |
| `\front\view\Languages` | `\front\Views\Languages` |
| `\front\view\Menu` | `\front\Views\Menu` |
| `\front\view\Newsletter` | `\front\Views\Newsletter` |
| `\front\view\Scontainers` | `\front\Views\Scontainers` |
| `\front\view\Search` | `\front\Views\Search` |
| `\front\view\Site` | → `\front\LayoutEngine` + `\front\Views\*` |
**Klasy Views NIE mają DI** — to czyste renderery statyczne (wzór: shopPRO).
### 4.4 Frontend Controllers
| Kontroler | Źródło | DI |
|-----------|--------|-----|
| `NewsletterController` | `front\controls\Newsletter` + `front\factory\Newsletter` | `NewsletterRepository` |
| `SearchController` | `front\controls\*` (jeśli istnieje logika) | brak |
| `ArticlesController` (opcjonalnie) | `front\controls\Articles` | `ArticleRepository` |
### 4.5 Przełączenie entry pointów
**`index.php`:**
```php
// Stare:
\front\controls\Site::check_url_params();
$out = \front\view\Site::show();
// Nowe:
\front\App::checkUrlParams();
$out = \front\LayoutEngine::show();
```
**`admin/index.php`:**
```php
// Stare:
\admin\Site::special_actions();
echo \admin\view\Page::show();
// Nowe:
\admin\App::special_actions();
\admin\App::update();
echo \admin\App::render();
```
---
## FAZA 5: Nowy system aktualizacji
### KLUCZOWA ZASADA: Pierwsza paczka musi być legacy!
**Pierwsza aktualizacja (v1.690) MUSI być opublikowana starą metodą** (ZIP + `_sql.txt` + `_files.txt`, BEZ manifestu). To dlatego, że v1.690 właśnie wgrywa nowy `UpdateRepository` z obsługą manifestów JSON na serwery klientów. Klienci mają jeszcze stary kod aktualizacji (`admin\factory\Update::update()`), który rozumie TYLKO legacy format.
**Sekwencja:**
1. **v1.690** — budujemy **ręcznie starą metodą** (lub `build-update.ps1` z flagą `--legacy-only`). Ta paczka zawiera nowy `UpdateRepository`, nowy `admin\App`, nowy autoloader — całą infrastrukturę.
2. **v1.691+** — od tego momentu budujemy normalnie z manifestem JSON. Klienci już mają nowy `UpdateRepository` (wgrany przez v1.690), który obsługuje oba formaty.
Innymi słowy: v1.690 to "paczka-most" — dostarczana starym kanałem, ale wgrywająca nowy silnik aktualizacji.
### 5.1 `Domain\Update\UpdateRepository` — pełna logika aktualizacji
**Plik:** `autoload/Domain/Update/UpdateRepository.php`
Skopiować z shopPRO `Domain\Update\UpdateRepository` i dostosować:
- URL serwera: `https://cmspro.project-dc.pl/updates/` (zamiast shoppro)
- Plik wersji: `libraries/version.ini` (jak jest teraz)
Kluczowe metody:
```php
public function update(): array
private function downloadAndApply(string $ver, string $dir, array $log): array
private function downloadAndApplyWithManifest(string $ver, string $dir, array $manifest, array $log): array
private function downloadAndApplyLegacy(string $ver, string $dir, array $log): array
private function verifyChecksum(string $filePath, string $expectedChecksum, array $log): array
private function createBackup(array $manifest, array $log): array
private function extractZip(string $fileName, array $log): array
public function runPendingMigrations(): void
```
**Kompatybilność wsteczna:** Metoda `downloadAndApply()` próbuje najpierw manifest. Jeśli brak → fallback do legacy (ZIP + _sql.txt + _files.txt). Dzięki temu stare paczki (v1.001-v1.689) nadal działają.
### 5.2 `admin\Controllers\UpdateController`
```php
namespace admin\Controllers;
use Domain\Update\UpdateRepository;
class UpdateController
{
private $repository;
public function __construct(UpdateRepository $repository)
{
$this->repository = $repository;
}
public function main_view(): string { /* widok panelu aktualizacji */ }
public function update(): void { /* POST: wykonaj update */ }
public function updateAll(): void { /* AJAX: pętla aktualizacji */ }
public function checkUpdate(): void { /* AJAX: sprawdź dostępność */ }
}
```
### 5.3 Nowy `updates/versions.php`
Dostosować do nowego formatu (wzór shopPRO):
```php
<?
$current_ver = 1690; // Pierwsza wersja z nowym systemem
for ($i = 1; $i <= $current_ver; $i++)
{
$dir = substr(number_format($i / 1000, 3), 0, strlen(number_format($i / 1000, 3)) - 2) . '0';
$version_new = number_format($i / 1000, 3);
if (file_exists('../updates/' . $dir . '/ver_' . $version_new . '.zip'))
$versions[] = $version_new;
}
// ... licencje (bez zmian) ...
```
### 5.4 Auto-generowany `updates/changelog.php`
Skopiować z shopPRO — skanuje `_manifest.json` + `changelog-legacy.json`.
### 5.5 `changelog-legacy.json`
Przenieść dotychczasowe wpisy z `updates/changelog.php` do JSON:
```json
[
{"version": "1.689", "date": "01.01.2026", "text": "- opis zmian v1.689"},
{"version": "1.688", "date": "...", "text": "- ..."},
...
]
```
### 5.6 `build-update.ps1`
Skopiować z shopPRO i dostosować:
- Zmienić header: `=== cmsPRO Build Update ===`
- Wersja: `v1.690` (format `X.XXX`, nie `0.XXX`)
- Katalog: `updates/1.60/` (dla 1.690), `updates/1.70/` (dla 1.700) itd.
- URL: `https://cmspro.project-dc.pl/updates/`
### 5.7 `.updateignore`
Skopiować z shopPRO, dostosować ścieżki:
```
*.md
docs/
CLAUDE.md
AGENTS.md
.claude/
.gitignore
.git/
tests/
phpunit.xml
phpunit.phar
composer.json
composer.lock
vendor/
test.ps1
memory/
updates/
.updateignore
build-update.ps1
migrations/
config.php
.htaccess
admin/.htaccess
libraries/version.ini
layout/style-css/style.css
layout/style-css/style.css.map
layout/style-scss/style.scss
layout/style-scss/_mixins.scss
layout/style-scss/_mixins.css
temp/
backups/
cache/
cron/temp/
.vscode/
.serena/
.phpunit.result.cache
```
### 5.8 Katalog `migrations/`
Migracje SQL w formacie `{version}.sql`:
```
migrations/
├── 1.690.sql
├── 1.691.sql
└── ...
```
`UpdateRepository::runPendingMigrations()` skanuje katalog i wykonuje migracje nowsze niż `version.ini`.
---
## FAZA 6: Testy PHPUnit
### 6.1 Struktura testów
```
tests/
├── bootstrap.php
├── stubs/
│ ├── CacheHandler.php ← stub dla \Shared\Cache\CacheHandler
│ └── Helpers.php ← stub dla \Shared\Helpers\Helpers
└── Unit/
└── Domain/
├── Article/
│ └── ArticleRepositoryTest.php
├── Settings/
│ └── SettingsRepositoryTest.php
├── User/
│ └── UserRepositoryTest.php
├── Languages/
│ └── LanguagesRepositoryTest.php
└── ...
```
### 6.2 Wzorzec testu
```php
class ArticleRepositoryTest extends TestCase
{
private $db;
private $repo;
protected function setUp(): void
{
$this->db = $this->createMock(\medoo::class);
$this->repo = new \Domain\Article\ArticleRepository($this->db);
}
public function testFindReturnsArticleArray(): void
{
$this->db->method('get')
->willReturn(['id' => 1, 'status' => 1]);
$result = $this->repo->find(1);
$this->assertIsArray($result);
$this->assertEquals(1, $result['id']);
}
public function testFindReturnsNullWhenNotFound(): void
{
$this->db->method('get')->willReturn(null);
$this->assertNull($this->repo->find(999));
}
}
```
---
## FAZA 7: Dokumentacja
Utworzyć w `docs/`:
- `PROJECT_STRUCTURE.md` — architektura, katalogi, namespace'y, routing
- `DATABASE_STRUCTURE.md` — schemat bazy danych (tabele `pp_*`)
- `TESTING.md` — jak uruchamiać testy
- `CHANGELOG.md` — historia zmian
- `UPDATE_INSTRUCTIONS.md` — jak budować paczki aktualizacji
---
## FAZA 8: Porządki (po pełnej migracji)
### 8.1 Usunięcie starych katalogów
```
rm -rf autoload/admin/controls/
rm -rf autoload/admin/factory/
rm -rf autoload/admin/view/
rm -rf autoload/front/controls/
rm -rf autoload/front/factory/
rm -rf autoload/front/view/
rm autoload/class.S.php
rm autoload/class.Tpl.php
rm autoload/class.Cache.php
rm autoload/class.Html.php
rm autoload/class.Image.php
rm autoload/class.Article.php
rm autoload/class.Page.php
rm autoload/class.Scontainer.php
rm autoload/admin/class.Site.php
```
### 8.2 Usunięcie wrappera `\S`
Przeszukać CAŁY kod i zamienić:
- `\S::get()` → `\Shared\Helpers\Helpers::get()`
- `\S::get_session()` → `\Shared\Helpers\Helpers::get_session()`
- `\S::alert()` → `\Shared\Helpers\Helpers::alert()`
- itd.
### 8.3 Usunięcie wrappera `\Tpl`
- `\Tpl::view(...)` → `\Shared\Tpl\Tpl::view(...)`
- `new \Tpl` → `new \Shared\Tpl\Tpl`
---
## PODSUMOWANIE — KOLEJNOŚĆ PRAC
| Faza | Co robimy | Szacunkowa złożoność |
|------|-----------|---------------------|
| **0** | Autoloader, katalogi, PHPUnit, git tag, CLAUDE.md | Mała |
| **1** | Shared utilities (S→Helpers, Tpl, Cache, Html, Image, Email) | Średnia |
| **2** | Domain repositories (16 modułów: Settings, Languages, Users, Pages, Articles...) | **Duża** |
| **3** | Admin Controllers + App router (16 kontrolerów) | **Duża** |
| **4** | Frontend App + LayoutEngine + Views + Controllers | Średnia |
| **5** | Nowy system aktualizacji (UpdateRepository, build-update.ps1, manifesty) | Średnia |
| **6** | Testy PHPUnit | Średnia |
| **7** | Dokumentacja | Mała |
| **8** | Porządki — usunięcie starych klas | Mała |
### Zasada naczelna
**Każda faza musi zostawić działający system.** Stary i nowy kod koegzystują dzięki:
- Fallback w routerze (nowy kontroler → stary controls)
- Wrappery w starych klasach (delegujące do nowych)
- Stopniowe przenoszenie referencji
### Kolejność prac wewnątrz fazy 2+3 (moduł po module)
Rekomendowana kolejność modułów (od najprostszych do najtrudniejszych):
1. **Settings** (1 metoda, zero zależności)
2. **Languages** (kilka metod, używany w bootstrap)
3. **Users** (login, 2FA, uprawnienia — kluczowe dla admin)
4. **Update** (nowy system aktualizacji)
5. **Pages** (routing frontu)
6. **Layouts** (layout engine)
7. **Scontainers** (kontenery statyczne)
8. **Banners** (banery)
9. **Authors** (autorzy)
10. **Menu** (menu — tylko front)
11. **Articles** (NAJTRUDNIEJSZY — class.Article.php z ArrayAccess, images, files, params, tags)
12. **Newsletter** (newsletter + preview renderer)
13. **SeoAdditional** (SEO per URL)
14. **Emails** (log kontaktowy)
15. **Backups** (backup bazy)
16. **ArticlesArchive** (archiwum — część ArticleRepository)
---
## CHECKLIST "KONIEC PRACY" (dla cmsPRO)
Po zakończeniu każdej sesji pracy:
1. Uruchom testy: `./test.ps1`
2. Zaktualizuj docs jeśli potrzeba
3. SQL migracje → `migrations/{version}.sql`
4. `updates/versions.php` — ustawić `$current_ver`
5. Commit + push
6. Git tag: `git tag v1.XXX` + `git push origin v1.XXX`
---
## RÓŻNICE cmsPRO vs shopPRO
| Aspekt | cmsPRO | shopPRO |
|--------|--------|---------|
| Cache | Plikowy (gzdeflate + MD5) | Redis (CacheHandler + RedisConnection) |
| Moduły sklepowe | BRAK | Product, Category, Order, Basket, Client, Coupon, Promotion, Transport, PaymentMethod, Producer, Attribute, ProductSet, ShopStatus, Integrations, Dashboard |
| Wersjonowanie | v1.689 (4 cyfry) | v0.308 (3 cyfry) |
| URL aktualizacji | cmspro.project-dc.pl | shoppro.project-dc.pl |
| API REST | Minimalny (api.php) | Rozbudowany (ApiRouter + 3 kontrolery) |
| Pixieset | TAK | NIE |
| AuditSEO | TAK | NIE |
| FormEdit System | NIE | TAK (ViewModels + FormValidator + FormFieldRenderer) |
| Remember-me | Legacy JSON cookie (hash hasła!) | HMAC-SHA256 signed payload |
**UWAGA na bezpieczeństwo:** W `admin/index.php` cmsPRO cookie remember-me przechowuje hash hasła w JSON — to luka! W ramach migracji Users należy to zmienić na HMAC-SHA256 (jak w shopPRO `admin\App::finalize_admin_login()`).