This post comes from the first version of this blog.
Please send me an email if anything needs an update. Thanks!

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:

1
./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():
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
    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):

1
            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:

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!