Pomoc - Szukaj - Użytkownicy - Kalendarz
Pełna wersja: [Biblioteka] Open Power Collector
Forum PHP.pl > Inne > Oceny
Zyx
Po długich perypetiach życiowych udało mi się wreszcie dokończyć pierwszą "normalną" bibliotekę z serii Open Power Libs 3. Nie jest ona może duża, ale ostatecznie nie zawsze potrzeba nam wielkich kobyłek. Tutaj wystawiam ją do publicznej oceny przez społeczność PHP.

Open Power Collector to uniwersalny interfejs do udostępniania różnych hierarchicznych danych. Można z niego zbudować system konfiguracji, albo np. gromadzić informacje o danym żądaniu HTTP. Jest on nastawiony na rozszerzanie i adaptację do nowych zastosowań. Uniwersalny interfejs dostępowy może dam całkiem dużo - nasz kod staje się dzięki temu niezależny od źródła danych konfiguracyjnych oraz tego, czym te dane tak właściwie są. Równie dobrze możemy je wczytywać z pliku konfiguracyjnego, generować dynamicznie na podstawie uprawnień, jak i w elegancki sposób testować w PHPUnit.

Przykładowe użycie w roli zbieracza danych o żądaniu HTTP:

  1. use Opl\Collector\Collector;
  2. use Opl\Collector\Visit\HostLoader;
  3. use Opl\Collector\Visit\ConnectionLoader;
  4.  
  5. $visit = new Collector();
  6. $visit->loadFromLoader(Collector::ROOT, new HostLoader());
  7. $visit->loadFromLoader(Collector::ROOT, new ConnectionLoader());
  8.  
  9. echo 'The user comes to us from '.$visit->get('ip').'.<br/>';
  10. echo 'He uses IPv'.$visit->get('ipVersion').' protocol<br/>';
  11. echo 'The connection is '.($visit->get('secure') ? '' : 'not').' secured with SSL.';


Przykładowe użycie w roli systemu konfiguracji:

  1. <?php
  2. use Opl\Collector\Collector;
  3. use Opl\Collector\Configuration\IniFileLoader;
  4.  
  5. $loader = new IniFileLoader('./config/');
  6. $loader->setFile('config.ini');
  7.  
  8. $collector = new Collector();
  9. $collector->loadFromLoader(Collector::ROOT, $loader);
  10.  
  11. if($collector->get('system.module.option'))
  12. {
  13. doSomething();
  14. }
  15. else
  16. {
  17. doSomethingElse();
  18. }


Obecnie zaimplementowana funkcjonalność:

- Możliwość gromadzenia i udostępniania danych hierarchicznych
- Możliwość wyciągnięcia całej grupy opcji
- Możliwość ładowania danych w dowolne miejsce hierarchii danych (tj. możemy dany plik konfiguracyjny doładować np. bezpośrednio do głównego poziomu, jak i podczepić jego zawartość pod jakiś węzeł).
- Obsługa cache'owania przy pomocy niewydanego jeszcze Open Power Cache
- Zestaw domyślnych ładowarek:
* Do budowy systemu konfiguracji: ładowanie danych z plików YAML, XML, INI.
* Do budowy zbieracza danych o żądaniu HTTP: informacje o zdalnym hoście, informacje o połączeniu.

Planowana funkcjonalność:

- Przyspieszenie dostępu do często używanych zagnieżdżonych opcji
- Ładowanie wartości jednej opcji z innej opcji
- Nowe ładowarki informacyjne o żądaniu HTTP; w szczególności chcę przeportować mechanizm rozpoznawania przeglądarki i systemu operacyjnego.

* Kod + bugtracker: Github
* Download
* Dokumentacja

Chętnie wysłucham kulturalnych opinii na temat tego projektu i możliwości jego dalszego rozwoju. Od razu też mówię: istnienie polskiej dokumentacji uzależnione jest od tego, czy ktoś ją po prostu przetłumaczy, przy czym ktoś != ja.
wookieb
FileLoader::__construct
  1. $length = strlen($path);
  2. if(0 == $length || DIRECTORY_SEPARATOR != $path[$length - 1])
  3. {
  4. $path .= DIRECTORY_SEPARATOR;
  5. }

Dlaczego nie "realpath" + .'/' ?

FileLoader
W FileLoaderze brakuje mi wsparcia dla ładowania wielu plików. Np mój katalog settings z plikami ini ma ich całkiem sporo,
gdybym chciał użyć twojego loadera to musiałbym naprawde nieźle się nagimnastykować aby zadbać o ładowanie odpowiednich plików w odpowiednim czasie.

LoaderyDynamiczne
Warto by pomysleć nad loaderami dynamicznymi (jak już), ponieważ to co jest teraz nie ułatwia mi pracy ponieważ równie dobrze mogę używać loadFromArray gdzie podam swoje źródło loaderów.

"no cache system installed"
  1. throw new BadMethodCallException('Cannot use Opl\\Collector\\Collector::save(): no cache system installed.');

Nie "installed" tylko "supplied" albo "assigned" i nie "cache system" tylko "cache object" coś w tym stylu. Aktualny komunikat jest mylący.

Collector loadFromLoader
Zapis
  1. $data = $loader->import();
  2. if(null === $path)
  3. {
  4. if(is_array($data))
  5. {
  6. $this->data = array_merge_recursive($this->data, $data);
  7. return true;
  8. }
  9. return false;
  10. }
  11. $partial = &$this->findKey($path);
  12. if(is_array($data))
  13. {
  14. $partial = array_merge_recursive($partial, $data);
  15. return true;
  16. }
  17. return false;


Skróciłbym do:

  1. $data = $loader->import();
  2.  
  3. if(null === $path) {
  4. $parentData = &$this->data;
  5. } else {
  6. $parentData = &$this->findKey($path);
  7. }
  8.  
  9. if(is_array($data))
  10. {
  11. $parentData = array_merge_recursive($parentData, $data);
  12. return true;
  13. }
  14. return false;


Chyba, że się mylę a wtedy łoj mnie równo


XML
  1. if(!isset($xmlElement['name']))
  2. {
  3. throw new XmlValidityException('Cannot load an XML file: the \''.$xmlElement->getName().'\' element has no \'name\' attribute.');
  4. }

Nie wiem czy nie lepiej zwalidować takie rzeczy za pomocą .XSD, ale to już zależy od podejścia

Cache Collectora
Collector jest "Keszowalny", że tak to brzydko nazwę, a wtedy brakuje mi do tego odpowiednich interfejsów/u jak np "Cacheable", który wprowadziłem u Siebie. Przykładowe metody
setCache(Interface_Cache $cache)
getCache()
setCacheKey($key);
getcacheKey($key)
setUseCache($useCache); // w formie dla vsprintf
getUsecache(); // czasem nie chce cacheować obiekt tylko na jakis czas, ale po co wtedy usuwac obiekt Cache?
generateCacheKey(array $parameters)

Jednoznacznie oznacza nam czy klasa obsługuje Cache czy też nie.
Aktualna obsługę cache można wywalić i lepiej zaimplementować interfejs Serializable a cacheowaniem zajmie się kto inny.

Visit/*
te dane raczej kompletnie nie nadaja się do "keszowania" moim zdaniem.

FileLoaderMock
Może nie jestem wybitnym specem od testów jednostkowych ale do czegoś takiego jest createMockForAbstractClass w PHPUnit-cie, jeżeli tego nie masz to https://github.com/sebastianbergmann/phpunit-mock-objects

Dodatkowo nie wiem czy dobrą praktyką jest testowanie właściwości "wewnętrznych" klasy za pomocą takich "mocków".
Jak już to na 100% brakuje mi metody getPaths w FileLoaderze. Nie uważam, jej za zbędną. A nawet gdyby jej nie było to nie wyobrażam sobie takiego testowania właściwości wewnętrznych, robisz to albo wykorzystując metody dostępne w klasie albo wcale (co jest raczej niemożliwe).

Co do "terminologii" to nie wiem czy nawet słowo "mock" się tutaj nadaje, ponieważ jak dla mnie jest to raczej klasa "zasób" do testów (tak samo jak pliki testowe (xml-e, ini, yaml-e)), czekam na Twoją opinie.

Provider
Bardzo dobry pomysł z drugiem parametrem metody "get". Sam miałem z tym problem, który ostatecznie nie rozwiązałem.


OGÓŁ
Ogólnie bardzo dobra robota. Gdybym miał to oceniać to solidne 9/10. Nie sprawdzałem czy testy pokrywają 100% kodu ale to już mniejsza z tym.
Aktualnie nie widzę większego sensu tego Collectora (bo brakuje dynamicznych loaderów). Osobiście nie użyję go, ale jako zastępca "Zend_Config" jak najbardziej OK.
Zyx
Dzięki za wyczerpującą odpowiedź. Już odpowiadam na pytania i wątpliwości:

FileLoader::__construct()

realpath() rozwija ścieżkę względną do absolutnej, co oznacza, że po prostu nie ma bata, ale musi on wykonywać powolne operacje dyskowe. Przy otwieraniu pliku i tak trzeba to porozwijać, ale wtedy system operacyjny zasadniczo wie, po co to rozwija i może to sobie jakoś (przynajmniej w teorii) zoptymalizować. Tymczasem jedyne czego potrzebuję to zwykły slash na końcu ścieżki i nic więcej.

Ładowanie wielu plików

To jest bardziej problem samego kolektora - ostatecznie musi on wiedzieć, w które miejsce drzewka zawartość każdego z tych plików podpiąć i naraz przetwarzać może tylko jedno źródło. Taką listę plików można zresztą wrzucić w tablicę i zapętlić:

  1. foreach(array('foo.ini', 'bar.ini', 'joe.ini') as $file)
  2. {
  3. $collector->loadFromLoader($loader->setFile($file));
  4. }


Loadery dynamiczne

O, dobrze że o tym wspomniałeś, bo zapomniałem o tym napisać. To też JEST planowane i nawet początkowo miało być od razu, ale doszedłem do wniosku, że najpierw lepiej jest zrobić stabilny podstawowy mechanizm, a później się bawić w jego rozbudowę o nowe bajerki, niż napakować za dużo i później się zastanawiać, jak zrobić, by to miało ręce i nogi.

"no cache system..."

OK, uwzględnione.

Collector loadFromLoader

Nie wiem, czy to właśnie nie zostało po pierwszym szkicu z dynamicznymi ładowarkami smile.gif. Niemniej masz rację, to można w taki sposób skrócić.

XML

XSD to już jest potężna kobyła. Wczytanie schematu, jego przeparsowanie, a później użycie zajęłoby trochę czasu. Wprawdzie niby takie czytanie i tak powinno być cache'owane, ale po sesji z Symfony 1.x stwierdziłem, że wersja robocza projektu ładująca się np. 8 sekund zdecydowanie przekracza mój poziom cierpliwości i nawet tu wymagam pewnej wydajności smile.gif. Struktura tych plików XML jest prosta, jak konstrukcja cepa, zaś sama metoda DomDocument::schemaValidate() zwraca nam jedynie true i false i nie mówi, co jest właściwie źle smile.gif.

Cache Collectora

Wyszedłem tu z założenia, że:

1. w systemie najczęściej i tak jest jeden obiekt cache,
2. jak już tworzymy taki kolektor i wprowadzamy do niego cache, to raczej nie po to, by te wszystkie parametry zmieniać 50 razy w trakcie trwania żądania.
3. jak ktoś będzie potrzebować cache, to od tego ma różne kontenery wstrzykiwania zależności i inne zabawki, a nie kolektor smile.gif.

Natomiast co do serializacji to masz rację. Taki interfejs na pewno się przyda, zarówno do różnych dziwnych zastosowań, o których mi się póki co nie śni, jak i do cache'owania, które można sobie po prostu obsłużyć zewnętrznie. Przy okazji zmniejsza się liczba zależności klasy.

Visit/*

I nikt nie zmusza Cię do ich cache'owania smile.gif.

FileLoaderMock

Że taka biblioteka jest, to wiem, bo bez niej trochę ciężko PHPUnit 3.5 zainstalować smile.gif. Wspomniana metoda jakoś mi umknęła podczas przeglądania dokumentacji, ale jej obecność faktycznie nieco zmienia postać rzeczy smile.gif.

Ad. testowania wewnętrznych właściwości -> "teoria" testowania ma wprowadzony podział na tzw. testy czarnej skrzynki i testy białej skrzynki. W tym drugim przypadku znamy wewnętrzną budowę testowanego elementu i używamy jej. W pewnych sytuacjach biała skrzynka jest pewniejsza; mógłbym tutaj oczywiście użyć którejś z klas pochodnych, ale wtedy w teorii istnieje ryzyko, że funkcjonalność dodana przez taką klasę może zaburzyć to, jak dana metoda działa w rzeczywistości. Testując bezpośrednio klasę abstrakcyjną jestem wolny od potencjalnych skutków ubocznych wywołanych nadpisaniem. Wiem, że omawiany przypadek jest skrajnie debilny i nawet bym się zdziwił, gdyby takie rzeczy się tu działy, ale po prostu postąpiłem według konwencji.
wookieb
Ładowanie wielu plików
No właśnie dlatego nie użyłbym czegoś takiego bo... nie zawsze potrzebuję wszystkich konfiguracji z całego drzewa katalogu.
Nawet jeżeli założeniem jest załadowanie całego drzewka to tworzenie takiej tablicy jest trudne. Lepiej dorobić metode "readEntireTree()", oczywiście wszystko zależy od twoich wewnętrznych założeń.
Dlatego loadery dynamiczne ratują sytuację.

xml
Ze schemaValidate można wyciągnąć błędy
  1. libxml_use_internal_errors(true);
  2. $dom->schemaValidate(...);
  3. // lista błędów
  4. libxml_get_errors()

Oczywiście to była tylko sugestia, ponieważ też w większości przypadków nie używam .XSD ale też nawet nie zwracam informacji o błędach w XML, dlatego zwróciłem na to uwagę smile.gif

Visit/*
Tak jasne, tylko troszkę zamydliło mi to koncepcję biblioteki. Z jednej strony ładujesz do niej pewne dane, które długi czas się nie zmienią (konfiguracje są raczej statyczne) a z drugiej dane typu HOST, IP których częstotliwość zmian jest bardzo duża. Po prostu dziwne miejsce na wykorzystanie pobierania TAKICH danych. A tym bardziej zapisywania ich w "cache" razem.

Testy
Co do teorii skrzynek to kiedyś o nich słyszałem, ale nie zapadło mi to w pamięć, aczkolwiek dzięki za przypomnienie choć nadal przystawałbym jednak do wersji "czarnej skrzynki" smile.gif

Zapomniałem dać jeszcze jednej pochwały.
METHOD CHAINING! O dzięki, bo nadal wiele osób go nie używa sad.gif
Ale... brak mi go w loadFromLoader, loadFromArray (nie wiem, czy ktoś będzie nawet sprawdzał wynik tej metody, lepszy jest wyjątek - moim zdaniem).
No i brakuje testu takiego czegoś. Niby pierd ale naprawdę dobry nawyk, który potrafi uchronić od wielu błędów
  1. $this->assertEquals($fileLoader, $fileLoader->setFile('abcdef.txt'));
Zyx
Błędy z libxml2 wyciąga się kosz-mar-nie smile.gif. Już wystarczy, że mam to w parserze XML-owym do OPT smile.gif.

Generalnie z tą biblioteką było tak, że początkowo miała się nazywać Open Power Visitor i zbierać tylko dane o wizycie, ale tak później popatrzyłem na to, pomyślałem i wymyśliłem, że identyczny interfejs dostępu do opcji będzie w konfiguracji, w jakichś ustawieniach prywatności użytkownika, czy ustawieniach personalizacyjnych. To wszystko działa tak samo, według tej samej logiki, więc po co do każdego przypadku użycia mam wynajdować koło od nowa? Zrobię jeden uniwersalny interfejs + możliwość tworzenia własnych ładowarek i będę mieć gotowe 10 rzeczy za jednym zamachem smile.gif.

Tak więc tutaj nie chodzi o to, czy dane zmieniają się nam co żądanie, czy raz na długi czas. Chodzi o to, że schemat pracy jest ten sam: wpychamy pewien zbiór informacji do tego kolektora, a później możemy sobie z niego w każdej chwili wygodnie czytać. Jeśli nam to jest potrzebne, to możemy go sobie scache'ować, ale to zależy tylko od nas i od konkretnego scenariusza zastosowania.

Ad. method chaining/fluent interface -> jak tylko nie zapomnę z rozpędu, to dodaję smile.gif. W loadFromArray() i loadFromLoader() faktycznie lepiej błędy raportować przez wyjątki i też dodać tam return $this, zatem również to sobie zakolejkuję do wykonania.
wookieb
Cytat
Zrobię jeden uniwersalny interfejs + możliwość tworzenia własnych ładowarek i będę mieć gotowe 10 rzeczy za jednym zamachem smile.gif.

Tak ale... bałbym się "rąbnięcia" czy czasem tego obiektu nie miałem wcześniej w Cache i co wtedy. Rozumiem, podejście "w jednym miejscu" ale idąc tym śladem, równie dobrze możesz tam trzymać obiekty modeli, formularzy, szablony itd.
Nie zrozum mnie źle, nie ganie Cię za podejście, lecz tylko chciałbym zwrócić uwagę na ryzyko popełnienia błędu. Oczywiście "pro" nie będą mieli z tym większego kłopotu ale reszta... ciężko.

W tym przypadku nie mam nic przeciwko gdyby nawet w Kontekście aplikacji albo też swoistemu rodzajowi "rejestrze" był sobie obiekt do zbierania danych wizyty bo TAM jest jego miejsce, STAMTĄD wiem, jaki obiekt zostanie zwrócony i dzięki TEMU obiektowi wiem jakie dane mogę pobrać (podpowiadanie IDE). Niepotrzebny mi jest wspólny interfejs.
Zyx
Ale wtedy tworzysz sobie dwa kolektory - jeden do konfiguracji, drugi do zbierania danych o wizycie. Gdyby to były zupełnie osobne implementacje, to i tak miałbyś dwa obiekty, a dodatkowo miałbyś dwie osobne klasy do załadowania.
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-2024 Invision Power Services, Inc.