Witajcie! Ze względu na to, że ostatnio mam coraz więcej wspólnego z Symfony2, możecie spodziewać się nieco więcej wpisów o tym frameworku na niniejszym blogu. Założyłem już kategorię “Symfony2”, a znajduje się w niej raptem jeden wpis, także czas nadrobić to niedociągnięcie. Dzisiaj chciałbym Wam pokazać jedno fajne usprawnienie, które pozwoli nam na lepszą kontrolę importowania danych mapowania encji z bazy danych do poszczególnych plików wybranego formatu. Zapraszam do lektury.

Fotografia: kopeckyk @ Fotolia.

Symfony2: Importowanie danych mapowania encji z filtrowaniem wyrażeniami regularnymi.

Tytuł dzisiejszego wpisu brzmi co najmniej jak nagłówek jakiegoś opracowania naukowego, ale spokojnie, nie bójcie się - wszystko będzie wytłumaczone jasno i prosto, jak to tylko możliwe. Znajomi z reguły kwitują tego typu stwierdzenia uniesieniem brwi, ponieważ potrafię z najbardziej błahego powodu zrobić kilkugodzinny wykład (szczególnie w temacie, który mnie interesuje :)), ale słowo pisane ma to do siebie, że widać, ile tekstu jest na ekranie, a więc będę się kontrolował. Tyle w kwestii wstępu, czas przejść “do rzeczy”.

Jak wiadomo, Symfony2 oferuje nam bardzo wygodne narzędzie konsolowe dostępne (w konsoli :)) poprzez polecenie:

./app/console

wykonane w katalogu głównym projektu. Mamy tam wiele funkcji, jedną z nich jest interesująca nas dzisiaj

doctrine:mapping:import
która pozwala na zaimportowanie danych poszczególnych encji do plików konfiguracyjnych używając jednego z obsługiwanych formatów. Ja preferuję pliki YAML, więc wszystkie polecenia będą się opierały na tym właśnie formacie - zwolenników XMLa i adnotacji proszę o “niewyzłośliwianie” się w komentarzach. :)

Przykładowy zapis tego polecenia wygląda następująco:

./app/console doctrine:mapping:import ThunderSampleBundle yml

Zaimportuje ono wszystkie dane tabel do encji o takich samych nazwach. Nie będę podawał przykładów wygenerowanych plików, bo wtedy ten wpis rozciągnąłby się niemiłosiernie - polecam spróbowanie samemu, jak to wszystko działa, a działa naprawdę genialnie. Warto jedynie nadmienić, że plik definicji encji zawiera pełną nazwę klasy encji, a więc także jej namespace. Dla tabeli “User” wyglądałoby to następująco:

Thunder\Bundle\SampleBundle\Entity\User

Problem.

W moim przypadku sytuacja wyglądała następująco: w MySQL Workbenchu przygotowałem sobie schemat bazy danych całego projektu i następnie chciałem zaimportować te dane jako encje do projektu Symfony2. Okazało się jednak, że poszczególne bundle (jeden bundel… dwa bundle? przyjmijmy, że to słowo przyjęło się w środowisku :)) będą używały poszczególnych tabel wybiórczo, a więc trzeba je odpowiednio posegregować.

Tutaj z pomocą przychodzi modyfikator “–filter” opisanej wyżej opcji importu, który potrafi przyjąć nazwy encji, jakie mają zostać umieszczone w danym bundlu (M. ten bundel, D. tego bundla, C. tym bundlu). Można go użyć wiele razy w jednym poleceniu, żeby przekazać różne nazwy:

./app/console doctrine:mapping:import ThunderSampleBundle yml --force --filter="Category" --filter="User"

Problem polega na tym, że (jak się okaże w dalszej części artykułu) na nazwach tych jest wykonywana bardzo prosta funkcja strpos(), a więc przechodzą wszystkie encje, jakie zawierają w sobie wartość parametru filter - w tym wypadku do bundla dostanie się np. encja Users, UserFriends, ItemCategories, itd.

Rozwiązanie.

Ze względu na fakt, że takie “nadmiarowe” umieszczanie encji w bundlach oraz późniejsze kasowanie niepotrzebnych plików jest mało zabawne, stwierdziłem, że “coś” z tym trzeba zrobić. Rozwiązanie pojawiło mi się automatycznie przed oczami - “a co, jeśli dałoby się filtrować po wyrażeniu regularnym?”. Znalazłem w kodzie Symfony2 klasę odpowiedzialną za komendę importowania danych encji:
Symfony\Bundle\DoctrineBundle\Command\ImportMappingDoctrineCommand
“Po nitce do kłębka” doszedłem do klasy:
Doctrine\ORM\Tools\Console\MetadataFilter
gdzie znalazłem metodę accept():

    public function accept()
    {
        if (count($this->_filter) == 0) {
            return true;
        }

        $it = $this->getInnerIterator();
        $metadata = $it->current();

        foreach ($this->_filter AS $filter) {
            if (strpos($metadata->name, $filter) !== false) {
                return true;
            }
        }
        return false;
    }

I zgadnijcie co? Zmieniłem warunek w pętli foreach na (cała nowa linijka):

            if (preg_match('/'.$filter.'/', $metadata->name)) {

i… śmiga! Biorąc pod uwagę, że jest to zmiana w rdzeniu frameworka, należałoby się zastanowić nad tym, czy nie spowoduje to błędu w innych miejscach w kodzie, ale rozważając dwa przypadki:

  • wykorzystanie “po staremu” modyfikatora “–filter” wpisując samą nazwę encji,
  • wykorzystanie wyrażenia regularnego,
stwierdzam, że w obu przypadkach wszystko zadziała tak, jak trzeba. Jeśli ktoś użyje samej nazwy, to dopasowanie /[nazwa]/ zadziała dokładnie tak samo, jak strpos(’[nazwa]‘) - dlatego dodaję w warunku ograniczniki wyrażenia, żeby to było ze sobą kompatybilne. Jeśli ktoś użyje wyrażeń regularnych… to użyje wyrażeń regularnych i wszystko jest ok. :)

Przykładowe wywołanie dla mojego przypadku wygląda następująco:

./app/console doctrine:mapping:import ThunderSampleBundle yml --force --filter="^Category" --filter="^User$"

W ten sposób zostaną zaimportowane jedynie encje rozpoczynające się od “Category” i pojedyncza encja “User”.

Oczywiście jeśli widzicie tutaj jakieś błędy, to proszę o wyjaśnienie, dlaczego nie mam racji - chciałbym zgłosić moją małą “kontrybucję” do repozytorium Symfony2, a nie chciałbym wyglądać jak pospolity klepacz przed samym Fabienem. :) Z góry dzięki i do zobaczenia w kolejnym wpisie!