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

33 KiB

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:

$f = 'autoload/' . implode('/', $q) . '/class.' . $c . '.php';

Docelowy — musi obsługiwać OBE konwencje (fallback PSR-4):

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

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:

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
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:
    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
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
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 UsersUserRepository 'Users' => fn() => new UsersController(new UserRepository($mdb))
2 SettingsController SettingsSettingsRepository 'Settings' => fn() => ...
3 UpdateController UpdateUpdateRepository 'Update' => fn() => ...
4 LanguagesController LanguagesLanguagesRepository
5 PagesController PagesPagesRepository
6 ArticlesController ArticlesArticleRepository
7 ArticlesArchiveController ArticlesArchive
8 LayoutsController LayoutsLayoutsRepository
9 ScontainersController ScontainersScontainersRepository
10 BannerController BannersBannerRepository
11 NewsletterController NewsletterNewsletterRepository
12 AuthorsController AuthorsAuthorRepository
13 EmailsController EmailsEmailRepository
14 BackupsController BackupsBackupRepository
15 SeoAdditionalController SeoAdditionalSeoAdditionalRepository
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:

// Stare:
\front\controls\Site::check_url_params();
$out = \front\view\Site::show();

// Nowe:
\front\App::checkUrlParams();
$out = \front\LayoutEngine::show();

admin/index.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:

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

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):

<?
$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:

[
    {"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

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 \Tplnew \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()).