- Jak wyglądają cenniki tabelaryczne?
- Najczęstsze problemy z parsowaniem cenników
- Prosta, uniwersalna klasa do obsługi cenników
- Funkcja cleanNumber – serce całego systemu
- Metoda getPrice – jak obliczamy cenę?
- Metoda getRange – poprawne min/max bez błędów
- Pełny kod klasy TablePrice (gotowy do użycia)
- Wydajność – jak przyspieszyć duże cenniki (10000+ komórek)?
- Czego nie robić – typowe błędy w konfiguratorach okien
- Podsumowanie
Jak obsłużyć cennik tabelaryczny od producenta? Jak poprawnie pobierać z niego cenę na podstawie szerokości i wysokości?
W konfiguratorach produktów na wymiar — niezależnie od tego, czy robimy okna, garaże, rolety, moskitiery, materace czy meble — cena najczęściej zależy od dwóch parametrów: szerokości i wysokości. I właśnie dlatego w tej branży królują cenniki tabelaryczne. Proste CSV, gdzie wiersze oznaczają wysokości, kolumny szerokości, a przecięcie to końcowa cena.
Problem zaczyna się wtedy, gdy do sklepu trafia realny plik od producenta: z formatowaniem tysięcy, spacjami twardymi NBSP, pustymi komórkami albo nagłówkami, które nie są liczbami. Efekt? Błędne min/max, ceny zwracające 0, a czasem cały konfigurator wysypany.
W tym artykule pokażę gotową, uniwersalną klasę PHP, która rozwiązuje te problemy. My w Design Cart podpinamy ją z powodzeniem w OpenCart, PrestaShop, WooCommerce oraz w Magento. Jest odporna na „dziwne” CSV i zawsze zwraca prawidłową cenę — nawet jeśli cennik wygląda jakby był sklejany w pięciu różnych firmach.
Jak wyglądają cenniki tabelaryczne?
Cenniki tabelaryczne to najprostszy możliwy model wyceny dla produktów tworzonych na wymiar. W praktyce wygląda to zawsze bardzo podobnie: wiersze odpowiadają wysokości, kolumny szerokości, a wartość w polu przecięcia to cena końcowa dla konkretnego rozmiaru. Dzięki temu sprzedawca może szybko przygotować wycenę, a konfigurator — natychmiast obliczyć koszt na podstawie dwóch parametrów.
Warto jednak pamiętać o jednym: pierwszy wiersz oraz pierwsza kolumna nie są częścią cennika, tylko nagłówkami, które określają dostępne rozmiary.
W praktyce takie cenniki pochodzą z różnych źródeł — Excel, Google Sheets, firmowe PDF-y konwertowane do CSV — dlatego często mają różne niedoskonałości. Spacje w liczbach, formatowanie tysięcy, spacje twarde (NBSP), a czasem nawet przypadkowe znaki sprawiają, że zwykłe intval() lub floatval() nie zdają egzaminu. Przykład: intval("1 200") zwróci 1, bo PHP odczyta tylko fragment przed pierwszą spacją.
Dlatego aby poprawnie wczytać i przetworzyć taki cennik, potrzebna jest klasa, która potrafi go oczyścić, zinterpretować i zamienić w uporządkowaną strukturę danych — nawet wtedy, gdy plik wygląda jak typowy „zakładowy” CSV od producenta.
Najczęstsze problemy z parsowaniem cenników
Cenniki tabelaryczne wyglądają banalnie tylko na pierwszy rzut oka. Poniżej omówię najczęstsze problemy, które potrafią wysadzić każdy konfigurator.
1. Spacje twarde (NBSP)
W wielu cennikach producenci stosują formatowanie tysięcy, np. 1 200, gdzie pomiędzy 1 a 200 wcale nie ma zwykłej spacji — to spacja twarda NBSP (\xC2\xA0).
Dla PHP to zupełnie inny znak, więc:
intval("1 200") → 1 Zamiast 1200 dostajesz 1, a to prowadzi do całej lawiny błędów:
-
nieprawidłowe min/max (wysokość minimalna wychodzi „1” zamiast np. 400)
-
złe indeksy w tablicach
-
ceny zwracające 0, bo wyszukiwany wymiar nie istnieje
NBSP to jeden z najczęstszych winowajców błędnych wycen w konfiguratorach.
2. Formatowanie tysięcy
Nawet jeśli spacje są „normalne”, to realne cenniki często wyglądają tak:
-
1 200 -
1 300 -
2 350
PHP czyta to w taki sposób:
intval("1 300") → 1 floatval("2 350") → 2 Zatem zanim zaczniemy cokolwiek parsować, trzeba usunąć wszystkie znaki niebędące cyframi.
W przeciwnym razie każdy wymiar lub cena zostanie zinterpretowana jako zupełnie inna liczba niż w rzeczywistości.
3. Nieprawidłowy pierwszy wiersz i pierwsza kolumna
To klasyk.
W poprawnym cenniku:
-
pierwsza kolumna zawiera wysokości (od wiersza 2)
-
pierwszy wiersz zawiera szerokości (od kolumny B)
A1 to zawsze nagłówek, często pusty lub z losową wartością w stylu „1”.
Jeśli ktoś tego nie uwzględni, minimalna wysokość wychodzi „1”, a minimalna szerokość „1”.
Efekt? Konfigurator:
-
podpowiada złe zakresy,
-
nie znajduje ceny dla wielu wymiarów,
-
daje ceny 0 lub błędnie wylicza najbliższe dostępne rozmiary.
Dlatego parser musi świadomie ignorować pierwszą komórkę i dopiero później analizować dane.
4. Puste komórki, przecinki, średniki
Pliki CSV z różnych firm potrafią używać różnych separatorów lub niestandardowych znaków:
-
przecinki
, -
średniki
; -
mieszankę spacji i tabulacji
-
puste komórki, które przerywają logikę tabeli
-
komórki z dodatkowymi opisami, np. „brak”, „niedostępne”, „—”
Parser, który nie jest na to przygotowany, natychmiast zacznie generować błędy lub zwracać błędne ceny.
Dlatego cenniki wymagają solidnego czyszczenia i normalizacji, zanim trafią do konfiguratora — inaczej nawet najprostsza tabela może wprowadzić chaos w całym procesie wyceny.
Prosta, uniwersalna klasa do obsługi cenników
W wielu projektach — od konfiguratorów okien, przez garaże, aż po materace i rolety — przerabialiśmy różne wersje cenników tabelarycznych. Każdy producent ma swój sposób zapisu danych, a CSV potrafi wyglądać zupełnie inaczej w zależności od źródła. W swoim repozytorium mamy tego naprawdę sporo. Niżej znajdziesz jedną z nich w uproszczonej wersji bo chodzi bardziej o logikę i zrozumienie tego jak można buszować wewnątrz plików CSV.
Najważniejsza zasada jest prosta: robimy wszystko raz, a potem już tylko korzystamy z gotowej struktury danych.
Jeden konstruktor → ścieżka do CSV
Przy tworzeniu obiektu podajesz tylko pełną ścieżkę do pliku CSV. Klasa sama zajmie się resztą: otworzy, odczyta i oczyści dane. Dzięki temu stosujemy ją zarówno w OpenCart, WooCommerce, PrestaShop, jak i w projektach customowych — bez żadnych zmian w logice.
Jedno wczytanie pliku → caching w pamięci
CSV jest wczytywany tylko przy pierwszym wywołaniu klasy.
Potem wszystkie operacje (wyszukiwanie ceny, zakresów, wymiarów) działają już wyłącznie na danych w pamięci. To ogromnie przyspiesza działanie konfiguratorów, szczególnie kiedy wycena jest wykonywana na AJAX w czasie rzeczywistym.
GetPrice() → dobiera najbliższy większy wymiar
Cenniki wymiarowe mają to do siebie, że użytkownik może podać dowolny rozmiar — 743 × 1287, 1529 × 982, itd.
Ale w cenniku takich wartości nie ma.
Dlatego metoda getPrice() szuka pierwszej szerokości ≥ width oraz pierwszej wysokości ≥ height, a następnie zwraca cenę ze skrzyżowania tych dwóch wartości. To dokładnie ta logika, którą stosują producenci i handlowcy w realnych wycenach.
GetRange() → zwraca min/max wysokości lub szerokości
Na podstawie oczyszczonych danych klasa potrafi zwrócić:
-
minimalną szerokość,
-
maksymalną szerokość,
-
minimalną wysokość,
-
maksymalną wysokość.
Co ważne — robi to poprawnie, pomijając nagłówki tabeli, które w wielu cennikach zawierają wartości mylące parser (np. „1”, „Wysokość”, „mm”).
Mechanizm cleanNumber() → rozwiązuje 99% problemów w CSV
Serce całej klasy to funkcja, która czyści każdą liczbę z:
-
spacji zwykłych,
-
spacji twardych NBSP,
-
formatowania tysięcy,
-
przecinków, kropek i myślników,
-
wszystkich znaków niebędących cyframi.
Cokolwiek znajdzie się w cenniku — 1 200, 1 300, 2 450 zł, 1.500, 1500,00 — po przetworzeniu zawsze dostajemy czystą, poprawną wartość liczbową.
Funkcja cleanNumber – serce całego systemu
Najwięcej problemów w cennikach powodują źle zapisane liczby: spacje, NBSP, formatowanie tysięcy czy dodatkowe znaki typu „zł” lub przecinki. Dlatego cała logika klasy opiera się na jednej prostej funkcji, która zamienia każdą wartość na czystą liczbę.
Co usuwa cleanNumber():
✔ zwykłe spacje
✔ spacje twarde NBSP (\xC2\xA0)
✔ kropki, przecinki i charakterystyczne formatowanie tysięcy
✔ wszystkie znaki niebędące cyframi
Dzięki temu klasa poprawnie parsuje praktycznie każdy cennik — niezależnie od tego, z jakiego programu pochodzi i jak został zapisany.
Kod:
private function cleanNumber($value) {
if ($value === null || $value === '') return 0;
$value = str_replace("\xC2\xA0", "", $value);
$value = str_replace(" ", "", $value);
$value = preg_replace('/\D+/', '', $value);
return $value === '' ? 0 : (float)$value;
}
Przykłady przed/po:
| Wejście | Wynik |
|---|---|
"1 200" |
1200 |
"1 400" (NBSP) |
1400 |
"2 345 zł" |
2345 |
"1500," |
1500 |
Metoda getPrice – jak obliczamy cenę?
Sama tabela nie wystarczy — użytkownik wpisuje dowolne wymiary, a cennik ma tylko określone skoki (np. 700, 800, 900…). Dlatego getPrice() działa w bardzo prosty sposób: wyszukuje najbliższy większy dostępny wymiar i z tego miejsca pobiera cenę.
Algorytm wygląda tak:
-
Znajdź pierwszą szerokość ≥ width
Jeśli szukamy 760, a w tabeli są 700, 800, 900… → trafiamy na 800. -
Znajdź pierwszą wysokość ≥ height
Jeśli szukamy 1330, a są 1300, 1400, 1500… → trafiamy na 1400. -
Zwróć cenę z przecięcia tych wartości
Czyli cena z komórki: wysokość 1400 × szerokość 800. -
Jeśli wymiar wykracza poza cennik → zwróć 0
(lub można rzucić wyjątek — zależnie od projektu).
Przykład z realnego cennika
Dla wymiarów:
-
width = 760
-
height = 1330
Cennik nie ma takich dokładnych wartości, więc klasa:
-
szuka pierwszej szerokości ≥ 760 → znajduje 800
-
szuka pierwszej wysokości ≥ 1330 → znajduje 1400
Następnie pobiera cenę ze skrzyżowania:
1400 (wys.) × 800 (szer.)
To dokładnie ten sam sposób zaokrąglania, którego używają producenci i sprzedawcy w wycenach manualnych.
Kod:
public function getPrice($width, $height) {
$this->load();
$col = null;
foreach ($this->widths as $i => $w) {
if ($w >= $width) {
$col = $i;
break;
}
}
$row = null;
foreach ($this->heights as $i => $h) {
if ($h >= $height) {
$row = $i;
break;
}
}
if ($row === null || $col === null) return 0;
return isset($this->prices[$row][$col]) ? $this->prices[$row][$col] : 0;
}
Metoda getRange – poprawne min/max bez błędów
W wielu cennikach pierwszy wiersz i pierwsza kolumna to nagłówki, a nie realne wymiary. Jeśli parser potraktuje je jak dane, pojawia się klasyczny błąd: minimalna wysokość wychodzi 1, mimo że pierwszy właściwy wymiar to 400. To samo potrafi dotknąć szerokości.
Rozwiązanie jest proste — podczas obliczania zakresów pomijamy pierwszy element tablicy wysokości, bo odpowiada on nagłówkowi, a nie wartości użytkowej. Dopiero kolejne pozycje reprezentują faktyczne dostępne rozmiary.
Dzięki temu getRange() zwraca poprawne minima i maksima, które można wykorzystać w formularzu konfiguratora, np. do ustawienia suwaków, walidacji i podpowiedzi. Użytkownik widzi więc dokładnie taki zakres, jaki faktycznie występuje w cenniku.
Kod:
public function getRange($type, $dimension) {
$this->load();
if ($dimension === 'width') {
$data = $this->widths;
} elseif ($dimension === 'height') {
$data = $this->heights;
if (isset($data[0])) {
array_shift($data);
}
} else {
throw new Exception("Invalid dimension: $dimension");
}
if (empty($data)) {
throw new Exception("No data for dimension: $dimension");
}
if ($type === 'min') return min($data);
if ($type === 'max') return max($data);
throw new Exception("Invalid range type: $type");
}
Pełny kod klasy TablePrice (gotowy do użycia)
Poniżej znajduje się kompletna, uproszczona i w pełni działająca wersja klasy TablePrice. Kod jest gotowy do użycia w projektach opartych na OpenCart, PrestaShop, WooCommerce lub w dowolnym własnym projekcie PHP. Klasa czyści dane, poprawnie obsługuje nagłówki i zwraca właściwe ceny oraz zakresy.
<?php
class tablePrice {
private $file;
private $widths = [];
private $heights = [];
private $prices = [];
private $loaded = false;
public function __construct($file_path) {
$this->file = $file_path;
}
private function cleanNumber($value) {
if ($value === null || $value === '') return 0;
$value = str_replace("\xC2\xA0", "", $value);
$value = str_replace(" ", "", $value);
$value = preg_replace('/\D+/', '', $value);
return $value === '' ? 0 : (float)$value;
}
private function load() {
if ($this->loaded) return;
if (!file_exists($this->file)) {
throw new Exception("Price file not found: " . $this->file);
}
if (($handle = fopen($this->file, "r")) !== FALSE) {
if (($header = fgetcsv($handle, 20000, ",")) !== FALSE) {
$width_raw = array_slice($header, 1);
$this->widths = array_map(function($v){
return $this->cleanNumber($v);
}, $width_raw);
}
while (($row = fgetcsv($handle, 20000, ",")) !== FALSE) {
$this->heights[] = $this->cleanNumber($row[0]);
$price_row_raw = array_slice($row, 1);
$this->prices[] = array_map(function($v){
return $this->cleanNumber($v);
}, $price_row_raw);
}
fclose($handle);
}
$this->loaded = true;
}
public function getPrice($width, $height) {
$this->load();
$col = null;
foreach ($this->widths as $i => $w) {
if ($w >= $width) {
$col = $i;
break;
}
}
$row = null;
foreach ($this->heights as $i => $h) {
if ($h >= $height) {
$row = $i;
break;
}
}
if ($row === null || $col === null) return 0;
return isset($this->prices[$row][$col]) ? $this->prices[$row][$col] : 0;
}
public function getRange($type, $dimension) {
$this->load();
if ($dimension === 'width') {
$data = $this->widths;
} elseif ($dimension === 'height') {
$data = $this->heights;
if (isset($data[0])) {
array_shift($data);
}
} else {
throw new Exception("Invalid dimension: $dimension");
}
if (empty($data)) {
throw new Exception("No data for dimension: $dimension");
}
if ($type === 'min') return min($data);
if ($type === 'max') return max($data);
throw new Exception("Invalid range type: $type");
}
}
Klasa działa od razu po wstawieniu jej do projektu. Wystarczy wskazać ścieżkę do pliku CSV i można korzystać z metod getPrice() oraz getRange().
Wydajność – jak przyspieszyć duże cenniki (10000+ komórek)?
Przy małych cennikach różnic nie widać, ale gdy tabela ma po kilkadziesiąt kolumn i setki wierszy, warto zadbać o optymalizację. Sama klasa działa szybko, ale można ją jeszcze przyspieszyć kilkoma prostymi technikami.
Cache wyników CSV do .json
Największy koszt to jednorazowe wczytanie CSV.
Można go zminimalizować, zapisując przetworzone dane (widths, heights, prices) do pliku .json.
Przy kolejnym wywołaniu system czyta gotową strukturę JSON zamiast ponownie analizować CSV.
To drastycznie skraca czas ładowania, szczególnie przy ciężkich plikach.
Wczytywanie tylko raz per request
Obecna klasa ładuje cennik tylko przy pierwszym odwołaniu ($this->loaded = true).
Dzięki temu każde kolejne wywołanie getPrice() lub getRange() działa już na danych w pamięci.
Przy konfiguratorach AJAX daje to natychmiastowe odpowiedzi.
Opcjonalne trzymanie cenników w APCu / Redis
W projektach z dużym ruchem można pójść krok dalej i trzymać przetworzoną tabelę w:
-
APCu (pamięć podręczna PHP),
-
Redis (idealne dla load balancerów i kilku instancji aplikacji).
Dzięki temu parser w ogóle nie dotyka pliku CSV — dane są dostępne w ułamku milisekundy.
Preload cenników w CRON, jeśli bardzo duże
Jeżeli cenniki są wyjątkowo rozbudowane (np. 10 000–50 000 komórek), warto generować cache JSON w tle:
-
CRON odczytuje CSV raz na dobę,
-
parser przetwarza plik,
-
gotowy cache zapisuje się na dysku.
Frontend i konfiguratory korzystają już wyłącznie z lekkiego, zoptymalizowanego pliku JSON.
Dzięki tym technikom nawet ogromne cenniki działają błyskawicznie, a konfigurator reaguje płynnie — niezależnie od liczby zapytań czy złożoności tabeli.
Czego nie robić – typowe błędy w konfiguratorach okien
Przy pracy z cennikami szerokość × wysokość łatwo popełnić kilka błędów, które później powodują błędne ceny, „0 zł” w konfiguratorze albo nieprawidłowe zakresy. Oto najczęstsze pułapki, których warto unikać.
Wczytywanie całego pliku CSV przy każdym AJAX
To jeden z największych killerów wydajności.
Jeśli konfigurator pobiera cenę na bieżąco, a przy każdym zapytaniu ponownie otwiera plik CSV, wynik to:
-
spowolnienia,
-
wysokie obciążenie serwera,
-
lagi przy dynamicznych zmianach wymiarów.
CSV powinien być wczytany tylko raz, a dalsze operacje wykonywane w pamięci.
Używanie floatval() i intval() dla liczb formatowanych
Wiele cenników ma zapis:
-
1 200 -
2 450 -
1500,00 -
2 345 zł
Proste parsowanie typu:
intval("1 200") → 1 floatval("2 345") → 2 zawsze skończy się błędami.
Najpierw trzeba usunąć spacje, NBSP i znaki niebędące cyframi.
Sortowanie bez czyszczenia liczb
Jeśli sortujesz liczby jako tekst, np.:
["1 200", "800", "2 000"] to kolejność po sortowaniu będzie zupełnie przypadkowa.
Zawsze czyść liczby przed sortowaniem — inaczej szerokości i wysokości mogą wylądować w złej kolejności i cennik będzie wyszukiwał nie te wartości, co trzeba.
Trzymanie cennika w MySQL (wolniejsze i trudniejsze w edycji)
Teoretycznie wygląda to dobrze, ale w praktyce:
-
edycja cennika staje się uciążliwa,
-
klienci nie poradzą sobie z aktualizacją,
-
cała tabela traci czytelność,
-
zapytania do bazy są wolniejsze niż praca na wczytanym CSV.
Najlepiej trzymać źródłowy plik CSV i dopiero z niego generować dane do konfiguratora.
Zapis UTF-8 z BOM → błędy w pierwszej kolumnie
Jeśli plik CSV został zapisany jako UTF-8-BOM, pierwszy wiersz zwykle zaczyna się od ukrytych bajtów BOM.
W praktyce parser widzi wtedy zamiast „400” coś w stylu:
"\xEF\xBB\xBF400" I traktuje to jako błędny ciąg.
Efekt: cennik nie ładuje się poprawnie albo min/max są obliczane błędnie.
Przed użyciem warto zapisać plik jako czysty UTF-8 bez BOM.
Unikanie tych kilku błędów sprawia, że cenniki działają stabilnie, a konfigurator zawsze zwraca prawidłowe ceny — niezależnie od tego, jak wygląda plik CSV od producenta.
Podsumowanie
Cenniki tabelaryczne to podstawowy sposób wyceny w branży okiennej i wszędzie tam, gdzie produkty powstają na wymiar. Największym źródłem problemów są nieprzewidywalne pliki CSV — spacje, spacje twarde, formatowanie tysięcy czy błędne nagłówki potrafią całkowicie zaburzyć działanie konfiguratora.
Prosta, uniwersalna klasa TablePrice eliminuje te problemy. Czyści liczby, poprawnie interpretuje nagłówki, dobiera właściwe wymiary i zwraca poprawne ceny. Działa w każdym sklepie i w każdym konfiguratorze, niezależnie od platformy.
Dzięki temu cenniki stają się szybkie, stabilne i odporne na błędy, a proces wyceny działa dokładnie tak, jak powinien — bez zaskoczeń dla użytkownika i bez chaosu w kodzie.
Sklepy internetowe Woocommerce
Sklepy internetowe Opencart
Sklepy internetowe Prestashop
Sklepy internetowe Magento
Strony internetowe Joomla!
Strony Internetowe Wordpress


