Pomoc - Szukaj - Użytkownicy - Kalendarz
Pełna wersja: [MySQL][PHP]Klasa wpisu.
Forum PHP.pl > Forum > Przedszkole
Sagnitor
Witam!

Niedawno rozpocząłem pisanie sobie zbioru przydatnych klas i funkcji, które roboczo nazywam moim mini 'frameworkiem'. Trafiła tam już m. in. klasa Uploader'a, którą pomogliście mi dopracować.
Stwierdziłem, że przy wielu projektach przydatna stanie się klasa wpisów inaczej newsów. Nowości występują w większości stron internetowych.
Przechodząc do sedna sprawy:

Myśląc abstrakcyjnie news posiada swoje prywatne pola jakimi są tytuł, treść, data itp. Jednak brakowało mi możliwości zarządzania wpisami tak bardziej 'z góry'.
Wymyśliłem klasę zarządzającą wpisami CNewsManager. Cała konstrukcja wydaje mi się o tyle korzystna (także pod względem wydajności), że do pobrania newsów wystarczy jedno zapytanie do bazy danych. Przejdźmy do kodu.

Klasa CNewsManager:
  1. class CNewsManager
  2. {
  3. private $aNewsBuffer = array(); // Bufor newsów
  4.  
  5. private $sTable; // Nazwa tabelki
  6. private $pdo; // Baza danych
  7.  
  8. public function __construct($pdo, $sTable)
  9. {
  10. $this->sTable = $sTable;
  11. $this->pdo = $pdo;
  12. }
  13.  
  14. /* Funkcja tworzą nowy wpis, zawartość rozszerzona pobierana jest opcjonalnie */
  15. public function createNews($title, $content, $exContent = NULL)
  16. {
  17. if($title !== NULL && $content !== NULL)
  18. {
  19. if($exContent !== NULL)
  20. {
  21. $oNew = new CNews($this->pdo, $this->sTable, NULL, $title, $content, NULL, $exContent);
  22. $oNew->upload();
  23. return $oNew;
  24. } else {
  25. $oNew = new CNews($this->pdo, $this->sTable, NULL, $title, $content, NULL);
  26. $oNew->upload();
  27. return $oNew;
  28. }
  29. } else return false;
  30. }
  31.  
  32. /* Funkcja ładująca wpisy do buffera newsów. Jeżeli nie podano id począštkowego i końcowego,
  33. zapytanie pobiera wszystkie rekordy z tabelki. ZAPYTANIE DO BAZY! */
  34. public function load($idStart = NULL, $idLimit = NULL)
  35. {
  36. if($idStart !== NULL && $idLimit !== NULL)
  37. {
  38. foreach($this->pdo->query('SELECT * FROM `'.$this->sTable.'` ORDER BY id DESC LIMIT '.$idStart.', '.$idLimit) as $row)
  39. {
  40. $this->aNewsBuffer[$row['id']] = $row;
  41. }
  42. return $this;
  43.  
  44. } else {
  45.  
  46. foreach($this->pdo->query('SELECT * FROM `'.$this->sTable.'`') as $row)
  47. {
  48. $this->aNewsBuffer[$row['id']] = $row;
  49. }
  50. return $this;
  51. }
  52. }
  53.  
  54. /* Funkcja opróżniająca buffer newsów */
  55. public function clearNewsBuffer()
  56. {
  57. for($i=0, $j=count($this->aNewsBuffer); $i < $j; ++$i)
  58. {
  59. $this->aNewsBuffer[$i] = NULL;
  60. }
  61. return $this;
  62. }
  63.  
  64. // Funkcja pobierająca obiekt newsa na podstawie podanego ID. Obiekt pobierany jest z bufora newsów.
  65. public function selectNews($nId)
  66. {
  67. if(isset($this->aNewsBuffer[$nId]))
  68. {
  69. $aNews = $this->aNewsBuffer[$nId];
  70. if($aNews['ex_tresc'] !== NULL)
  71. {
  72. $oNews = new CNews($this->pdo, $this->sTable, $aNews['id'] , $aNews['tytul'], $aNews['tresc'], $aNews['data'], $aNews['ex_tresc']);
  73. return $oNews;
  74. } else {
  75. $oNews = new CNews($this->pdo, $this->sTable, $aNews['id'] , $aNews['tytul'], $aNews['tresc'], $aNews['data']);
  76. return $oNews;
  77. }
  78. } else return false;
  79. }
  80.  
  81. /* Wyrenderowanie wszystkich wpisów zawartych w bufferze */
  82. public function showAll()
  83. {
  84. for($i=1, $j=count($this->aNewsBuffer); $i<$j; ++$i)
  85. {
  86. if($this->aNewsBuffer[$i] !== NULL)
  87. {
  88. if($this->aNewsBuffer[$i]['ex_tresc'] !== NULL)
  89. {
  90. $oNews = new CNews($this->pdo, $this->sTable, $this->aNewsBuffer[$i]['id'], $this->aNewsBuffer[$i]['tytul'], $this->aNewsBuffer[$i]['tresc'], $this->aNewsBuffer[$i]['data'], $this->aNewsBuffer[$i]['ex_tresc']);
  91. $oNews->show();
  92. } else {
  93. $oNews = new CNews($this->pdo, $this->sTable, $this->aNewsBuffer[$i]['id'], $this->aNewsBuffer[$i]['tytul'], $this->aNewsBuffer[$i]['tresc'], $this->aNewsBuffer[$i]['data']);
  94. $oNews->show();
  95. }
  96. }
  97. }
  98. return $this;
  99. }
  100.  
  101. }


Klasa CNews:

  1. class CNews
  2. {
  3. private $id; // ID wpisu
  4. private $date; // Data
  5. private $title; // Tytuł
  6. private $content; // Zawartość podstawowa
  7. private $exContent; // Zawartość rozszerzona
  8. private $author; // Autor wpisu
  9.  
  10. private $table;
  11. private $datebase; // Baza danych (PHP Data Objects)
  12.  
  13. /* Konstruktor klasy wpisu. OPTYMALIZACJA! */
  14. public function __construct($datebase, $sTable, $nId, $strTitle, $strContent, $date, $strExContent = NULL)
  15. {
  16. $this->datebase = $datebase;
  17. if ($strTitle !== NULL && $strContent !== NULL && $sTable !== NULL)
  18. {
  19. $nId === NULL ? $this->id = NULL : $this->id = $nId;
  20. $date === NULL ? $this->date = date('d.m.Y, H:i') : $this->date = $date;
  21. $this->title = $strTitle;
  22. $this->content = $strContent;
  23. $this->table = $sTable;
  24. $strExContent === NULL ? $this->exContent = NULL : $this->exContent = $strExContent;
  25. /* Jeszcze autor */
  26. return true;
  27. } else return false;
  28.  
  29. }
  30.  
  31. public function edit($strTitle, $strContent, $strExContent = NULL)
  32. {
  33. if($strTitle !== NULL && $strContent !== NULL)
  34. {
  35. $this->title = $strTitle;
  36. $this->content = $strContent;
  37. if($strExContent !== NULL)
  38. $this->exContent = $strExContent;
  39. } else return false;
  40. }
  41.  
  42. /* Funkcja aktualizująca wpis w bazie danych.
  43. Zależnie od statusu news jest tworzony lub uaktualniany.
  44. ZAPYTANIE DO BAZY! */
  45. public function upload()
  46. {
  47. if($this->title !== NULL && $this->content !== NULL && $this->table !== NULL)
  48. {
  49. if($this->id !== NULL)
  50. {
  51. $this->datebase->exec("UPDATE ".$this->table." SET tytul='".$this->title."', tresc='".$this->content."', ex_tresc='".$this->exContent."' WHERE id='".$this->id."'");
  52. return true;
  53. } else {
  54. $this->datebase->exec("INSERT INTO ".$this->table." VALUES(0,'".$this->title."','".$this->content."', '".$this->exContent."', '".$this->date."', '".$this->author."')");
  55. return true;
  56. }
  57. } else return false;
  58. }
  59.  
  60. public function delete()
  61. {
  62. if($this->id !== NULL)
  63. $this->datebase->exec("DELETE FROM ".$this->table." WHERE id='".$this->id."'");
  64. }
  65.  
  66. /* Funkcja pobierająca tytuł */
  67. public function getTitle()
  68. {
  69. return ($this->title !== NULL ? $this->title : false);
  70. }
  71.  
  72. /* Funkcja pobierająca zawartość podstawowąš */
  73. public function getContent()
  74. {
  75. return ($this->content !== NULL ? $this->content : false);
  76. }
  77.  
  78. public function getExContent()
  79. {
  80. return ($this->exContent !== NULL ? $this->exContent : false);
  81. }
  82.  
  83. public function show()
  84. {
  85. echo '<tr>
  86. <td><h2>'. $this->title .'</h2>
  87. <span>'. $this->date .'</span>
  88. </td>
  89. </tr>';
  90. if($this->exContent === NULL)
  91. echo '
  92. <tr>
  93. <td colspan="2"><div class="wiadomosc">'. $this->content .'</div></td>
  94. </tr>';
  95. else
  96. echo '<tr>
  97. <td colspan="2"><div class="wiadomosc">'. $this->content .'<div style=" float: right; margin-right: 35px; margin-top: 15px;"><a href="?home=home&amp;more='.$this->id.'">Czytaj więcej...</a></div></div></td>
  98. </tr>';
  99. }
  100.  
  101. public function show_ex()
  102. {
  103. echo '<tr>
  104. <td><h2>'. $this->title .'</h2>
  105. <span>'. $this->date .'</span>
  106. </td>
  107. </tr>
  108. <tr>
  109. <td colspan="2"><div class="wiadomosc">'. $this->exContent .'</div></td>
  110. </tr>';
  111. }
  112. }


Tutaj pojawia się moje pytanie: czy koniecznością jest przekazywanie takiej ilości parametrów do konstruktora CNews?
Chciałbym także poznać wasze opinie co do budowy i mojego sposobu myślenia w tym przypadku. Chętnie dowiem się jakie rozwiązania proponujecie oraz jakie porady macie dla mnie co do pisania kodu.

Teraz skrawek kodu przykładowego:

  1. $pdo = new PDO('mysql:host=localhost;dbname=newsy', 'root', '');
  2.  
  3. $NewsManager = new CNewsManager($pdo, 'news');
  4. $NewsManager->load(1,11);
  5. $NewsManager->selectNews(5)->show();


Pozdrawiam

Jakieś propozycje?

PS. Nie wiem czy temat umieściłem w odpowiednim dziale, w razie problemów proszę moderatora o przeniesienie.

Już niedługo dodam także klasę komentarzy.

Pozdrawiam
thek
Powiem tak... Niemal większość rzeczy w Twoim managerze powinna być implementowana przez CNews wink.gif
CreateNews - to tylko opakowanie tworzenia newsa, na dodatek extendedContent powinna sama klasa CNews wyłapać, a nie, że różne konstruktory wywołujesz. Konstruktor i tak sprawdza czy tam jest jakaś wartość, więc nawet jeśli wywołasz ją w klasie nadzorczej to i tak konstruktor będzie sprawdzał, czy ona jest. Bo tak konstruktor napisałeś.

Load - za ładowanie całej tabeli w przypadku braku początku i końca, to za jaja by Cię trzeba było powiesić wink.gif W takiej sytuacji powinieneś ładować ostatnich X newsów. To co chcesz robić to tylko na desktopie, gdzie masz taki obiekt cały czas w pamięci, a nie, że tworzony za każdym razem od nowa z każdym żądaniem! To wstęp do paginacji. Inna sprawa, że powinieneś się tam zastanowić nad sensem przeciążenia metody load() lub stworzenie kilku wariantywnych:
loadSingle - ładuje jeden wybrany,
loadRange - ładuje zakres od-do po id lub kolejności w bazie danych - Twój wybór,
loadDefault - ładuje X ostatnich,
loadChosen - ładuje wybrane przez użytkownika.

ClearNewsBuffer - po co pętla? $this->$aNewsBuffer = array(); i pozamiatane

SelectNews - pisałem wyżej o load...

ShowAll - znowu extendedContent rozróżniane jak w CreateNews, grrrrr... Poza tym "dziedziczy" ona błąd klasy CNews... Mieszanie modelu z widokiem. Od kiedy model na pałę wartości wali w kod HTML? A to robi CNews->show()

Poza tym gratuluję pomysłu "chainingu"... SelectNews( nieistniejąceId )-> show();
Jak zrobisz show() na wartości false?

Ja już nawet nie ruszam tematu: "Chłopaki... Nie możemy użyć RDBMS. Musimy przejść na pliki XML jako formę przechowania danych." i jedno wielkie "K..wa mać! Framework tego nie przewiduje!"
Sagnitor
Wielkie dzięki za rozjaśnienie kilku istotnych rzeczy.

Zgodzę się, że:
-CreateNews - fakt, zbytnio obudowałem to klasą nadzorującą, lecz czy po usunięciu wywołania dwóch konstruktorów takie obudowanie jest niewłaściwe?
- Load - kilka przeładowań tej funkcji. Wolę uniknąć wieszania za jaja :-),
- ClearNewsBuffer - no tak, jakieś przyzwyczajenie po destruktorze w C++.

Jeżeli chodzi o resztę mam kilka pytań. Powiedzmy, że załadowałem $NewsManager->loadDefault(x), x ostatnich wpisów i chcę operować na jednym (załadowanym już w bufforze, aby nie operować na zapytaniach). Myślałem właśnie nad utworzeniem funkcji pokroju $NewsManager->selectNews(), lecz w przypadku błędnego ID wszystko się sypie.

Spodziewałem się wytknięcia błędu jak to nazwałeś 'mieszania modelu z widokiem', lecz zupełnie nie mam pojęcia o co w tym chodzi. W sensie, że klasa nie powinna mieć metody 'renderującej'?

Jeszcze raz dzięki za w ogóle pofatygowanie się w czytaniu moich wypocin.

Pozdrawiam

EDIT:
Czyli funkcja loadCokolwiek, powinna zwracać obiekt CNews w przypadku jednego wyniku lub tablicę obiektów jeżeli rezultat jest większy od jednego?
thek
Zależy jak spojrzeć z tym obudowaniem... Ja bym czegoś takiego jak dodawanie i edycja czy usunięcie pojedynczego elementu nawet nie dodawał do klasy nadzorującej. Nadzorca robi to hurtowo, a więc edytujesz, usuwasz i dodajesz elementy kolekcji od razu jako wielokrotności. Jeśli miałbym coś w takim nadzorcy dodać to jedynie odwołania do klasy podlegającej. W środowisku Web byłoby to odwołanie się do kontrolera danej klasy. Innymi słowy gdybym chciał dodać nowy obiekt, to nie z poziomu zarządcy bym go tworzył, ale zwrócił się bezpośrednio do samej klasy podrzędnej. O co mi chodzi? Skoro masz klasę CNews, to na bank ma ona swój formularz dodawania i edycji. I to do niego bym kierował, a nie jakiejś sztucznej w nadzorcy, która robi dokładnie to samo. Innymi słowy przekierowanie na metodę create klasy CNews i jej własny widok smile.gif

Jeśli chcesz select, to czemu w tym momencie zamiast id nie użyjesz offsetu lub indeksu tablicowego? I tak używasz tablicy... Jaka z tego korzyść? Próba odwołania do nie istniejącego elementu mogła by być sygnalizowana wyjątkiem.

Klasa nie tyle nie powinna mieć metody renderującej co powinna mieć własny widok lub kilka widoków i to do nich się odwoływać. Sam powiedz... Jeśli zrobiłbym taki render jak Ty walnąłeś, to skąd miałbym pewność, że dopasuje mi się to do całej strony? Walnąłeś bowiem wszystko w tr->td a skąd mam mieć pewność, że ów widok trafi do strony, która mi to opakuje potem w tablicę?

Wszystkie funkcje load powinny zwracać tablicę. Choćby z jednym tylko elementem. No chyba, że loadSingle mógłby zwrócić obiekt klasy CNews i wtedy ładnie byś miał od razu z mostu dostęp do zmiennej this. Ale z racji zgodności z innymi wariantami obstawałbym za tablicą i tam odwołaniem do iteratora i/lub metody current(). No i w razie tablicy mógłbym przewijać się po elementach metodami prev(), next(), current()
Sagnitor
Teraz zrozumiałem bezsensowność zapisywania w tablicy pod indeksem równym ID. Dzięki jeszcze raz za rady.
Niedługo może wrzucę tu zmodyfikowaną i poprawioną wersję tej pełnej nieporozumień klasy wink.gif.

Na początku tworząc w ogóle obiektowy system wpisów, stworzyłem samą klasę CNews w troszkę innej formie. Brakowało mi właśnie takiego 'nadzorcy', który robiłby to hurtowo, jednak w trakcie pisania CNewsManager widocznie zapomniałem o jego przeznaczeniu.

Z metod load, będę zwracać tablice z danymi newsów, bo po co wywoływać X konstruktorów i tworzyć tablicę obiektów, skoro można powybierać.
Zastanawiam się jeszcze nad metodą loadChosen(). Sednem sprawy są parametry (nie wiadomo ile użytkownik ich przekaże). Pogrzebię u wujka Google na ten temat.

Tak, czy siak kliknąłem zasłużone oraz symboliczne "Pomógł".

Pozdrawiam
Sagni
thek
EDIT:
Cytat
ClearNewsBuffer - no tak, jakieś przyzwyczajenie po destruktorze w C++
Miałem to samo wink.gif Na szczęście szybko przeszedłem nad tym do porządku dziennego. Inna sprawa, że robiąc tak jak Ty proponowałeś, doprowadziłbyś do powstania tablicy NULLi, co też nie jest prawidłowe.

Jeszcze odniosę się do propozycji:

klasa CNews:
metody:
konstruktor, add, update, delete i settery oraz gettery dla poszczególnych pól
Większość tego już masz, ale nie zawsze prawidłowo. To czego Ci brakuje to brak obiektu walidacji, kontroli uprawnień i warstwy abstrakcji na zapis. Kompletnie olałeś te aspekty. Operacja na danych powinna być przeniesiona do jakiegoś abstrakcyjnego bytu, który dopiero decydowałby czy operacje działają jako plikowe czy na bazie danych. Gdyby tak przyjąć, to operacje add i update magła by być jedną metodą save(), która obiekt klasy CNews przekazała by wyżej gdzieś do warstwy modelu. On by się już sam tym zajął. I to w owej warstwie abstrakcji byś decydował na czym tak naprawdę pracujesz. Ów obiekt abstrakcyjny bym wstrzykiwał do klas z danymi pracującymi. Zobacz może jakiś ORM by mniej więcej załapać o co w tym mniej więcej chodzi.
Przypuśćmy klasa Storage, która działa wedle wzorca Strategia nie byłaby tu głupim pomysłem. Używałbyś zawsze tego samego obiektu, ale implementacja konkretnego silnika bazodanowego ( nawet poprzez kolejna abstrakcję, jaką jest PDO ) lub operacji plikowych byłaby dla klienta niewidoczna.

Zależy co chcesz osiągnąć... Czasem tablica obiektów mogła by być lepsza. Czemu? Przypuśćmy takie wywołanie
  1. $manager = new CNewsManager(); //domyślny konstruktor, tylko utworzenie obiektu i niczego innego
  2. $manager->loadDefault(); //domyślne załadowanie jakąś ilością newsów
  3. $manager->next()->next()->current()->getTitle(); //pobranie tytułu z 3 (bo dwukrotne użycie next) newsa.

Zauważ ciekawą rzecz... Iteruję metodami next po tablicy obiektów i gdy chcę konkretny daję current, co zwraca mi wprost obiekt klasy CNews, a więc mogę użyć jego własnego gettera getTitle. Mogłem użyć też przypuśćmy zamiast dwóch next() własnej metody index(2), która w przypadku niepowodzenia sypnęła by mi wyjątkiem. Tak samo obwarowałbym wyjątkami ogólnie iterację po tablicy wyników.
Sagnitor
Cytat
To czego Ci brakuje to brak obiektu walidacji, kontroli uprawnień i warstwy abstrakcji na zapis.


Prostą przyczyną braku tych aspektów jest brak wiedzy, jak się za to zabrać i z czym to się je. wink.gif
Nigdy nie projektowałem jakiś większych frameworków, także wykładam się w tym miejscu brakiem doświadczenia. Widzę, że projektowanie relacji pomiędzy klasami szybko może się przeistoczyć w coś większego.

Jak na razie po kilku zmianach (m. in. utworzeniu metody save(), zamiast add() i upload() ), wyciąganie danych wygląda w ten sposób:

  1. $pdo = new PDO('mysql:host=localhost;dbname=newsy', 'root', '');
  2. $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
  3.  
  4. $NewsManager = new CNewsManager($pdo, 'news');
  5. try
  6. {
  7. $aNews = $NewsManager->loadDefault(10);
  8. }
  9. catch(Exception $e)
  10. {
  11. echo 'Błąd menedżera wpisów!:'. $e->getMessage() .' w linii '. $e->getLine();
  12. }
  13.  
  14. for($i=0, $j=count($aNews); $i<$j; ++$i)
  15. {
  16. echo '<h1>'.$aNews[$i]->getTitle().'</h1>';
  17. echo '<h3>'.$aNews[$i]->getDate().'</h3>';
  18. echo '<p>'.$aNews[$i]->getContent().'</p>';
  19. }


Zmieniłem także dodawanie parametrów w konstruktorze CNews (tytuł, zawartość itp) na opcjonalne, w razie tworzenia nowego newsa wykorzysta się $News->save($title, $content itp).

EDIT:
Masz może jakieś tutoriale lub książki traktujące o takiej warstwowości?

Pozdrawiam
thek
Osobiście nie spotkałem jakichś podręczników do tego. To kwestia dużej ilości czytania, między innymi na temat wzorców, oraz grzebania wewnątrz kodu frameworków, czyli raczej coś, co dla początkujących osob jest mało przyjemne. Myślę, ze na początek powinieneś poczytac o samym wzorcu Strategia (a także innych podstawowych), by zrozumieć o co w nim chodzi. Wtedy złapiesz jak to mniej więcej można zaimplementować. Gdybyś to zaimplementował, to zapewne wyszedłbyś z połaczeniami do jakiegoś pliku konfiguracyjnego, gdzie byś raz a porządnie określał wszystko o formie (file, mysql, pdo czy xml) źródła danych i potem FW sam by się tego trzymał.

Co do uprawnień to poczytaj choćby tutaj wpisy i artykuły o ACL oraz rozwiń to. A walidacja... Nieco napomknięto w artykule o refaktoryzacji na wortalu. Ogólnie jednak lepiej podejrzeć jak to robią większe FW. No cóż... Niestety w programowaniu wiele rzeczy nie jest ładnie opisanych i zrozumiale. Najwięcej człowiek uczy się przegladając kod dużych projektów, a to nie jest dla wielu ani łatwe, ani przyjemne. Można poczytać książki, można znaleźć trochę tutoriali, ale one tylko najprostsze rzeczy przedstawią i może ciut ostrzegą przed wpadkami poważnymi, ale porządnej implementacji tam raczej nie uświadczysz.
Sagnitor
Czytając teraz troszkę o Strategii, zrozumiałem, że gdybym zrobił Interface INews o metodzie save();, to potem tworząc dwie klasy implementujące ten interface, w różny sposób wykonywana by była ta czynność. Z tego co zrozumiałem sugerujesz dodać klasę/warstwę abstrakcji zapisu, która wczytywałaby z pliku config, w jaki sposób dokonywać zapisu wpisów oraz innych czynności. Dobrze rozumuję? wink.gif
thek
No widzisz... Czytanie się opłaca smile.gif Co do konfiga, to myślałem o tym, by podczas żądań strona sobie z konfiga czytała czego używa i wybierała odpowiednią implementację obsługi źródła danych. Dla całości serwisu miałbyś bowiem dostępne metody zdefiniowane w interfejsie i tylko ich byś używał. To JAK by to było od strony silnika rozwiązane byłoby już problemem samej klasy. Dla Twojego kodu istniały by tylko wywołanie obiektu klasy Storage i jej metod. To jednak jaka klasa by była w ramach Storage odpowiedzialna za reakcję w danej chwili, działoby się "pod maską". Mógłbyś też poczytać o wzorcu Factory (metoda wytwórcza), Abstract Factory i Builder, bo je też można wykorzystać i nawet sądzę, że któraś z nich będzie lepsza w Twoim wypadku. Abstract Factory daje Ci transparentność kosztem jednak skomplikowania (masz jeden interfejs od strony klienta, ale w środku możesz mieć nawalone od groma zależności wink.gif ), Factory daje większą swobodę ( duża rozszerzalność i niezależność ), a Builder skupia sie bardziej na utworzeniu obiektu, ale dzięki temu mogą to być bardzo skomplikowane i zróżnicowane twory, a sam wzorzec łatwo się skaluje, ponieważ istnieje w nim pewien nadzorca kierujący całym procesem tworzenia. Dzięki temu całość jest super przenośna, nawet na środowiska wieloprocesorowe czy klastry. Poczytaj trochę o wszystkich i oceń co u Ciebie najbardziej się będzie liczyć oraz wybierz właściwe rozwiązanie.
To jest wersja lo-fi głównej zawartości. Aby zobaczyć pełną wersję z większą zawartością, obrazkami i formatowaniem proszę kliknij tutaj.
Invision Power Board © 2001-2025 Invision Power Services, Inc.