SOCJAL
Do góry

Ćwiczenia w programowaniu aplikacji internetowych w Symfony

Symfony to jeden z najlepszych dostępnych obecnie frameworków w języku PHP. Dzięki jasnej strukturze oraz generatorom kodu przygotowanie kompletnej aplikacji WWW zajmuje kilku minut. Artykuł opisuje krok po kroku przebieg wykonania internetowego katalogu aparatów fotograficznych.

Cel ćwiczenia

Celem opisywanego zadania jest wykonanie aplikacji internetowej, która służy do gromadzenia danych o aparatach fotograficznych:

  • nazwa modelu (np. EOS 400D),
  • nazwa producenta (np. Nikon),
  • rodzaj matrycy (np. CMOS),
  • cena (np. 3299 zł).

Wykonana witryna ma składać się z dwóch niezależnych części. Pierwsza, nazywana frontend, będzie służyła do przeglądania bazy danych bez możliwości wprowadzania zmian. Dostęp do frontendu nie wymaga zalogowania. Każda tabela bazy danych będzie zawierała dwa rodzaje podstron: listę wszystkich rekordów tabeli oraz szczegółowe dane jednego wybranego rekordu.

Druga część witryny, tzw. backend, będzie panelem administracyjnym, służącym do edycji zawartości bazy danych. Dostęp do backendu zabezpieczymy formularzem autoryzacyjnym. Edycja rekordów bazy danych będzie możliwa tylko po zalogowaniu. Aplikacja będzie zawierała jedno konto administracyjne o nazwie admin i haśle kartofel.

Krok pierwszy: utworzenie bazy danych

Pracę nad aplikacją rozpoczynamy od utworzenia bazy danych oraz konta dostępu. Zadanie to realizują dwa skrypty:

tworzenie-bazy-aparaty.sql
tworzenie-bazy-aparaty.bat

W pliku tworzenie-bazy-aparaty.bat należy w miejsce AX1BY2CZ3 wprowadzić hasło administratora serwera MySQL na komputerze localhost.

Po wykonaniu skryptu tworzenie-bazy-aparaty.bat na lokalnym serwerze MySQL zostanie utworzona baza danych o nazwie aparaty. Baza ta będzie wypełniona przykładowymi rekordami. Dodatkowo na serwerze MySQL zostanie utworzone konto o nazwie fotograf i haśle pstryk. Będzie ono miało wszelkie uprawnienia do bazy danych aparaty. Konto fotograf jest tworzone następującym zapytaniem SQL:

GRANT 
  ALL ON aparaty.* 
  TO fotograf@localhost 
  IDENTIFIED BY \'pstryk\';

Dostęp do bazy danych zapewnia następujący DSN (ang. Data Source Name):

mysql://fotograf:pstryk@localhost/aparaty

Po wykonaniu skryptu tworzenie-bazy-aparaty.bat sprawdzamy, czy baza danych aparaty została zainstalowana. Powinna zawierać 3 tabele i 33 rekordy, o czym informuje phpMyAdmin.

Krok drugi: pobranie i rozpakowanie pliku sf_sandbox.tgz

Sandbox to spakowane archiwum, przygotowane z myślą o początkujących użytkownikach frameworka Symfony. Zawiera kompletny kod frameworka i pusty projekt o nazwie sf_sandbox. Korzystanie z sandboxa wymaga instalacji Apache z mod_rewrite oraz PHP.

Korzystanie z pakietu Sandbox jest bardzo proste, gdyż sprowadza się do dwóch kroków: pobrania oraz wypakowania pliku {stala}sf_sanbox.tgz{/stala}.

Ze strony projektu Symfony http://www.symfony-project.org kopiujemy plik sf_sandbox.tgz (znajduje się w dziale Installation).

Wypakowujemy go w publicznym folderze serwera Apache (np. htdocs/). Następnie przeglądarką internetową odwiedzamy stronę:

localhost/sf_sandbox/web

Ujrzymy domyślną stronę projektu Sandbox.

Dostosowywanie panelu administracyjnego

Moduły CRUD generowane poleceniem:

symfony propel-generate-crud

dostosowywaliśmy do własnych potrzeb, modyfikując kod PHP (usunęliśmy kilka metod z pliku actions.class.php) oraz szablony (zmieniliśmy pliki {stala}listSuccess.php{/stala} oraz {stala}showSuccess.php{/stala}).

W przypadku modułów administracyjnych tworzonych poleceniem:

symfony propel-init-admin

postępujemy nieco inaczej. Wygląd i zachowanie modułów definiujemy w plikach konfiguracyjnych .yml. Moduł wygenerowany dla każdej tabeli ma własny plik konfiguracyjny. Pliki konfiguracyjne modułów aparat, matryca i producent aplikacji backend znajdują się w folderach:

sf_sandbox/apps/backend/modules/aparat/config/generator.yml
sf_sandbox/apps/backend/modules/matryca/config/generator.yml
sf_sandbox/apps/backend/modules/producent/config/generator.yml

W pliku konfiguracyjnym modułu aparat wprowadzamy zawartość:

generator:
  class:              sfPropelAdminGenerator
  param:
    model_class:      Aparat
    list:
      title:          Lista aparatóww
      display:        [ =model, producent, matryca, cena ]
      max_per_page:   10

Wpis title to etykieta umieszczana powyżej tabelki, max_per_page ustala liczbę rekordów na stronie, zaś display definiuje kolumny zawarte w tabeli z zestawieniem rekordów. Symbol = przed nazwą kolumny model powoduje, że nazwa modelu aparatu będzie hiperłączem do strony ze szczegółowymi danymi aparatu.

W pliku konfiguracyjnym tabeli Matryca umieszczamy wpisy:

generator:
  class:              sfPropelAdminGenerator
  param:
    model_class:      Matryca
    list:
      title:          Lista matryc
      display:        [ =nazwa ]
      max_per_page:   4

zaś panel administracyjny tabeli Producent konfigurujemy:

generator:
  class:              sfPropelAdminGenerator
  param:
    model_class:      Producent
    list:
      title:          Lista producentów
      display:        [ =nazwa ]
      max_per_page:   6

Ponownie uruchamiamy generatory:

symfony propel-init-admin backend aparat Aparat
symfony propel-init-admin backend matryca Matryca
symfony propel-init-admin backend producent Producent

czyścimy pamięć podręczną:

symfony cc

i odwiedzamy przeglądarką panel administracyjny:

http://localhost/sf_sandbox/web/backend.php/aparat

Autoryzacja

Oczywiście dostęp do panelu administracyjnego należy zabezpieczyć.

Najpierw dodajemy pliki konfiguracyjne modułów aparat, matryca oraz producent:

sf_sandbox/apps/backend/modules/aparat/config/security.yml
sf_sandbox/apps/backend/modules/matryca/config/security.yml
sf_sandbox/apps/backend/modules/producent/config/security.yml

W każdym z trzech powyższych plików umieszczamy identyczny wpis:

default:
is_secure: on

Spowoduje to całkowite zablokowanie dostępu do panelu administracyjnego. Jeśli wyczyścimy pamięć podręczną i odwiedzimy panel administracyjny, to ujrzymy komunikat: dostęp wymaga zalogowania. Ponieważ jednak aplikacja backend nie zawiera jeszcze formularza do logowania, zatem cały panel administracyjny będzie niedostępny.

Formularz do logowania

Dodajemy nowy moduł o nazwie security aplikacji backend:

symfony init-module backend security

Następnie przygotowujemy szablon akcji index. W folderze:

sf_sandbox/apps/backend/modules/security/templates/

tworzymy plik o nazwie {stala}indexSuccess.php{/stala}. Wprowadzamy w nim następujący kod:

Autoryzacja

hasErrors()): ?> Błędna próba logowania! Spróbuj ponownie! get(\'login\')) ?>

Zwróćmy uwagę na wywołanie funkcji {stala}form_tag(){/stala}. Jej parametrem jest akcja odpowiedzialna za przetwarzanie formularza. Jest to akcja o nazwie login modułu security, co zapisujemy:

security/login

W pliku {stala}sf_sandbox/apps/backend/modules/security/actions/actions.class.php{/stala} dodajemy metody obsługujące akcje login oraz logout. Akcja login zawiera zaszyte na stałe w kodzie dane konta administracyjnego: login == admin oraz password == kartofel:

public function executeLogin()
{
  if (
    $this->getRequestParameter(\'login\') == \'admin\' && 
    $this->getRequestParameter(\'password\') == \'kartofel\'
  ) {
    $this->getUser()->setAuthenticated(true);
    return $this->redirect(\'aparat\');
  } else {
    $this->getRequest()->setError(\'login\', \'incorrect entry\');
    return $this->forward(\'security\', \'index\');
  }
}  

public function executeLogout()
{
  $this->getUser()->setAuthenticated(false);
  return $this->redirect(\'security/index\');
}

Na zakończenie dodajemy w układzie aplikacji backend (tj. w pliku {stala}sf_sandbox/apps/backend/templates/layout.php{/stala}) instrukcję if wraz z hiperłączem do wylogowania:

isAuthenticated()): ?>
  

oraz ustalamy stronę główną aplikacji backend (plik {stala}sf_sandbox/apps/backend/config/routing.yml{/stala}):

homepage:
  url:   /
  param: { module: security , action: index }

Uwagi

Opisane ćwiczenie zostało przetestowane na platformie Windows XP/Apache 2.2/PHP 5.2/MySQL 5.0. Wykonanie aplikacji trwa krócej niż jednostka zajęć uniwersyteckich z programowania (tj. 2 x 45 minut).

Jest to nieco inny model pracy od tradycyjnego programowania. Zamiast pisać kod – generujemy go, a otrzymany szkielet dostosowujemy do własnych potrzeb. Stosując trzy generatory:

  • propel-build-model do generowania aktywnych rekordów,
  • propel-generate-crud do generowania szkieletu aplikacji frontend,
  • propel-init-admin do generowania panelu administracyjnego.

możemy otrzymać działający szkielet aplikacji dedykowanej potężnej bazie danych w kilka minut. Takiej wydajności nigdy nie uda się osiągnąć pisząc własny kod.

Wykonując ćwiczenie pamiętajmy, że po wprowadzeniu modyfikacji w plikach .yml musimy wyczyścić pamięć podręczną (polecenie symfony cc). Pamiętajmy też, że Sandbox nie będzie działał, jeśli którykolwiek z folderów zawiera dziwne znaki, np.:

htdocs\mój projekt ver. 0.1.2\sf_sandbox\...

Znakami, które nie powodują kłopotów są litery, cyfry podkreślenia i myślniki.

Tytuły podstron oraz kodowanie znaków można zmienić w plikach konfiguracyjnych view.yml. Plik {stala}view.yml{/stala} możemy umieszczać w folderze konfiguracyjnym aplikacji (np. {stala}sf_sandbox/apps/frontend/config/view.yml{/stala}) jak i modułu (np. {stala}sf_sandbox/apps/frontend/modules/aparat/config/view.yml{/stala}) Pliki .yml mogą stosować kodowanie utf-8.

W przypadku błędów korzystajmy ze środowisk przeznaczonych do debuggowania aplikacji. Mają one adresy:

http://localhost/sf_sandbox/web/frontend_dev.php/
http://localhost/sf_sandbox/web/backend_dev.php/

Plik {stala}symfony-cwiczenie.txt{/stala}, który zawiera listę kolejnych kroków wraz z nazwami modyfikowanych plików, ułatwi powtórne wykonanie ćwiczenia.

Krok piąty: aplikacja backend

Rolę panelu administracyjnego będzie pełniła aplikacja o nazwie backend. Pierwszym krokiem jest utworzenie nowej pustej aplikacji o nazwie backend. W tym celu wydajemy polecenie:

symfony init-app backend

Następnie generujemy trzy moduły służące do edycji zawartości tabel aparat, matryca oraz producent:

symfony propel-init-admin backend aparat Aparat
symfony propel-init-admin backend matryca Matryca
symfony propel-init-admin backend producent Producent

Moduły panelu administracyjnego są dostępne za pośrednictwem URL-i:

http://localhost/sf_sandbox/web/backend.php/aparat
http://localhost/sf_sandbox/web/backend.php/matryca
http://localhost/sf_sandbox/web/backend.php/producent

Globalna nawigacja

Układ aplikacji backend jest zapisany w pliku:

sf_sandbox/apps/backend/templates/layout.php

Dodajemy w nim menu główne:

Ponieważ menu to jest identyczne, jak w aplikacji frontend, możemy skopiować cały plik {stala}sf_sandbox/apps/frontend/templates/layout.php{/stala} i wkleić do folderu {stala}sf_sandbox/apps/backend/templates/{/stala}.

Metody __toString()

Rozszerzamy klasy Aparat, Matryca oraz Producent zawarte w folderze {stala}sf_sandbox/lib/model/{/stala}. W każdej z nich definiujemy metodę {stala}__toString(){/stala}. W klasie Aparat metoda {stala}__toString(){/stala} może zwracać model aparatu:

class Aparat extends BaseAparat 
{
  function __toString()
  {
    return $this->getModel();
  }
}

zaś w klasach Matryca oraz Producent – nazwę:

class Matryce extends BaseMatryca 
{
  function __toString()
  {
    return $this->getNazwa();
  }
}
class Producent extends BaseProducent
{
  function __toString()
  {
    return $this->getNazwa();
  }
}

Po wprowadzeniu zmian ponownie uruchamiamy generator paneli administracyjnych:

symfony propel-init-admin backend aparat Aparat
symfony propel-init-admin backend matryca Matryca
symfony propel-init-admin backend producent Producent

czyścimy pamięć podręczną:

symfony cc

i odwiedzamy panel administracyjny przeglądarką. Edytor szczegółowych danych aparatu będzie zawierał teraz listy rozwijane z nazwami producentów oraz nazwami matryc (poprzednio listy rozwijane zawierały nic niemówiące identyfikatory – klucze pierwotne id).

Krok trzeci: dostęp do bazy danych

Po pobraniu i rozpakowaniu Sandboksa przechodzimy do pracy nad witryną poświęconą aparatom fotograficznym. Pierwszym krokiem jest zdefiniowanie źródła danych oraz struktury bazy. Należy zmodyfikować zawartość trzech plików:

sf_sandbox/config/databases.yml 
sf_sandbox/config/propel.ini
sf_sandbox/config/schema.yml

Pliki o rozszerzeniu .yml zawierają informacje konfiguracyjne w języku YAML. Są to pliki tekstowe, w których wcięcia nadają danym strukturę.

W plikach {stala}databases.yml{/stala} oraz {stala}propel.ini{/stala} należy wprowadzić adres źródła danych:

all:
  propel:
    class:      sfPropelDatabase
    param:
      phptype:  mysql
      dsn:      mysql://fotograf:pstryk@localhost/aparaty
propel.database            = mysql
propel.database.createUrl  = mysql://fotograf:pstryk@localhost/aparaty
propel.database.url        = mysql://fotograf:pstryk@localhost/aparaty

Natomiast w pliku schema.yml definiujemy strukturę bazy danych. Baza danych zawiera trzy tabele o nazwach aparat, matryca oraz producent. Każda z tabel zawiera klucz pierwotny o nazwie id, który będzie podlegał autoinkrementacji. Tabela aparat jest połączona z tabelami matryca oraz producent relacjami 1:n. Klucze obce relacji otrzymują nazwy matryca_id i producent_id. Opis bazy danych przyjmuje postać:

plik sf_sandbox/config/schema.yml
propel:
  aparat: 
    id:
    producent_id: 
    matryca_id: 
    model: varchar(20)
    cena: varchar(20)
  matryca: 
    id: 
    nazwa: varchar(20)
  producent: 
    id: 
    nazwa: varchar(20)
Uwaga! Białe znaki w plikach o rozszerzeniu .yml odgrywają bardzo ważną rolę. Wcięcia należy wykonać stosując wielokrotność dwóch spacji (nie można stosować tabulatorów).

Po przygotowaniu plików {stala}databases.yml{/stala}, {stala}propel.ini{/stala} oraz {stala}schema.yml{/stala} wygenerujemy klasy zapewniające dostęp do bazy danych. W tym celu uruchamiamy wiersz poleceń (Start/Uruchom/cmd), a następnie przechodzimy do folderu sf_sandbox/:

C:
cd \
cd C:\Program Files\Apache Software Foundation\Apache2.2\htdocs\sf_sandbox

Klasy dostępu do bazy danych generuje polecenie:

symfony propel-build-model

Po jego wydaniu w folderze sf_sandbox/lib/model/ pojawią się pliki:

Aparat.php
AparatPeer.php
Matryca.php
MatrycaPeer.php
Producent.php
ProducentPeer.php

Zawierają one klasy wygenerowane przez aplikację Propel dla modelu opisanego w pliku {stala}schema.yml{/stala}. Klasy te mają następujące metody zapewniające dostęp do pól:

Aparat
    getId()
    getModel()
    getCena()
    getMatryca()
    getMatrycaId()
    getProducent()
    getProducentId()
    
Matryca
    getId()
    getNazwa()
    
Producent
    getId()
    getNazwa()

Jeśli w skrypcie PHP dostępna jest zmienna klasy {stala}$aparat{/stala}, to korzystając z powyższych metod możemy wydrukować model aparatu:

echo $aparat->getModel();

nazwę producenta:

echo $aparat->getProducent()->getNazwa();

lub matrycy:

echo $aparat->getMatryca()->getNazwa();

Krok czwarty: aplikacja frontend

Witryna poświęcona aparatom fotograficznym będzie składała się z dwóch niezależnych członów. Jeden z nich – frontend – będzie umożliwiał przeglądanie zawartości katalogu bez możliwości wprowadzania zmian. Drugi – backend – będzie panelem administracyjnym, służącym do edycji zawartości bazy danych. Człony te w terminologii Symfony są nazywane aplikacjami i znajdują się w folderze {stala}sf_sandbox/apps/{/stala}.

Sandbox zawiera jedną (pustą) aplikację o nazwie frontend. (Jeśli zajrzymy do folderu {stala}sf_sandbox/apps/{/stala}, to znajdziemy tam folder frontend/.) Dlatego aplikacji tej nie musimy tworzyć. Przystępujemy do wygenerowania modułów dostępu do trzech tabel bazy danych. W tym celu w wierszu poleceń (w folderze sf_sandbox/) wydajemy komendy:

symfony propel-generate-crud frontend aparat Aparat
symfony propel-generate-crud frontend matryca Matryca
symfony propel-generate-crud frontend producent Producent

Po wykonaniu powyższych poleceń odwiedzamy strony:

http://localhost/sf_sandbox/web/aparat
http://localhost/sf_sandbox/web/matryca
http://localhost/sf_sandbox/web/producent

(Powyższe adresy trzeba ręcznie wpisać w polu Adres przeglądarki). Będą one prezentowały zawartość bazy danych. Moduły wygenerowane poleceniem {stala}propel-generate-crud{/stala} należy teraz dostosować do własnych potrzeb, modyfikując kod PHP oraz szablony.

Wygenerowane moduły CRUD pozwalają na tworzenie, edycję, uaktualnianie oraz usuwanie rekordów. Poszczególne operacje mają następujące adresy URL:

/show/id/X
/update/id/X
/delete/id/X
/create/id/X

W adresach tych X jest identyfikatorem rekordu, zaś nazwą modułu, np.:

http://localhost/sf_sandbox/web/aparat/show/id/5
http://localhost/sf_sandbox/web/producent/delete/id/1

Polecenie {stala}propel-generate-crud{/stala} tworzy moduł CRUD (Create, Retrieve, Update, Delete) dla podanej tabeli. Pierwszym parametrem jest nazwa aplikacji, drugim – nazwa modułu, a trzecim – nazwa klasy dostępu (wygenerowanej przez Propel):

symfony propel-generate-crud   

Wygenerowany moduł zostaje zapisany w folderze:

sf_sandbox/apps//modules/

na przykład:

sf_sandbox/apps/frontend/modules/aparat

Globalne menu

Dostosowywanie aplikacji frontend rozpoczynamy od wykonania menu głównego. Skórka aplikacji frontend jest zawarta w pliku:

sf_sandbox/apps/frontend/templates/layout.php

Zmieniamy jego zawartość, dodając następujące menu główne:


getRaw(\'sf_content\') ?>

Hiperłącza w szablonach Symfony umieszczamy wykorzystując funkcję {stala}link_to(){/stala}. Jej pierwszym parametrem jest etykieta hiperłącza, zaś drugim – adres URL. Jeśli w aplikacji wygenerowano moduł o nazwie lorem i akcję o nazwie ipsum, to hiperłącze przyjmie adres:

Jeśli jako adres podajemy tylko nazwę modułu:

to hiperłącze będzie prowadziło do akcji domyślnej modułu.

W przypadku akcji create, edit, update, delete modułów CRUD poprawnymi adresami są:

/?id=X

na przykład:


Style CSS

Zmieniamy style CSS aplikacji frontend. W pliku:

web/css/main.css

usuwamy całą zawartość i wprowadzamy:

#pojemnik {
    width: 800px;
    margin: 0 auto;
}

#menu {
    width: 300px;
    float: left;
}

#content {
    width: 400px;
    float: right;
}

Odwiedzamy przeglądarką WWW dowolny z modułów aplikacji frontend, np. matryca:

http://localhost/sf_sandbox/web/matryca

Ujrzymy witrynę, która będzie prezentowała zawartość trzech tabel bazy danych. Każda z podstron będzie zawierała menu główne, pozwalające na dostęp do wszystkich tabel.

Dostosowanie wyglądu modułu

Przechodzimy do dostosowania wyglądu modułu aparat. Najpierw w pliku {stala}sf_sandbox/apps/frontend/modules/aparat/actions/actions.class.php{/stala} usuwamy metody:

executeEdit()
executeCreate()
executeUpdate()
executeDelete()

Uniemożliwi to edycję rekordów tabeli aparat modułem CRUD. Następnie zmieniamy szablony modułu CRUD dla tabeli aparat. Są one zawarte w folderze: {stala}sf_sandbox/apps/frontend/modules/aparat/templates/{/stala}.

Usuwamy plik {stala}editSuccess.php{/stala}, zaś w pliku {stala}listSuccess.php{/stala} wprowadzamy kod:

Aparaty

  • getModel(), \'aparat/show?id=\'.$aparat->getId() ) ?>

Modyfikację wyglądu modułu CRUD tabeli aparat kończymy wprowadzając w pliku {stala}showSuccess.php{/stala} zawartość przedstawioną na listingu 1. W podobny sposób modyfikujemy moduły CRUD tabel producent oraz matryca.

Aparat: getModel() ?>

Producent: getProducent()->getNazwa(), \'producent/show?id=\' . $aparat->getProducentId() ) ?>
Model: getModel() ?>
Matryca: getMatryca()->getNazwa(), \'matryca/show?id=\' . $aparat->getMatrycaId() ) ?>
Cena: getCena() ?>

Zmiana strony głównej aplikacji

Ostatnią zmianą w aplikacji frontend jest ustalenie domyślnej strony głównej. W pliku {stala}sf_sandbox/apps/frontend/config/routing.yml{/stala} wpisujemy:

homepage:
  url:   /
  param: { module: aparat, action: index }

Spowoduje to, że po odwiedzeniu adresu:

http://localhost/sf_sandbox/web/

ujrzymy listę aparatów, czyli akcję index (wpis action: index) modułu aparat (wpis module: aparat).

Sprawdzamy działanie aplikacji frontend.

Zostaw komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *

Więcej w Internet Maker

  • Jak szybko i łatwo stworzyć stronę mobilną

    W dzisiejszych czasach coraz bardziej popularne staje się przeglądanie stron internetowych na urządzeniach mobilnych. Ba! Samo surfowanie po sieci to w...

    Mateusz Chorążewicz14/11/2014
  • Strona na WordPressie? Grunt to dobry hosting

    WordPress jest powszechnie uważany za najlepszy system zarządzania treścią zarówno dla prostych blogów i stron, jak i większych serwisów internetowych oraz sklepów....

    Mateusz Chorążewicz14/11/2014
  • Jak zabezpieczyć swoje zdjęcia w chmurze

    Niedawny wyciek nagich zdjęć znanych osobistości wznowił dyskusje na temat bezpieczeństwa naszych prywatnych danych. Jako że część fotografii została wykradziona poprzez...

    Mateusz Chorążewicz02/09/2014
  • Jak usunąć historię swojej aktywności z Facebooka?

    Usunięcie swojej aktywności z Facebooka nie jest takie proste, jakby się mogło wydawać. Portal społecznościowy daje możliwość zachowania kopii swoich postów...

    Mateusz Chorążewicz03/01/2014
  • Pozbądź się spamu z pluginem MotionCAPTCHA

    Obecnie captcha jest jednym z najlepszych sposobów na zatrzymanie spamu. W niniejszym artykule przestawię troszkę inną jej odmianę, niż zazwyczaj możemy...

    Martyna Kilian22/08/2012