Sf2: korzystanie z usługi wewnątrz podmiotu

Wiem, że to było zadawane w kółko, czytam tematy, ale zawsze koncentruje się na konkretnych przypadkach i ogólnie staram się zrozumieć, dlaczego nie jest najlepszą praktyką korzystania z usługi wewnątrz jednostki.

Biorąc pod uwagę bardzo prostą obsługę:

Class Age
{
  private $date1;
  private $date2;
  private $format;

  const ym = "%y years and %m month"
  const ...


  // some DateTime()->diff() methods, checking, formating the entry formats, returning different period formats for eg.
  }

I byt prosty:

Class People
{
  private $firstname;
  private $lastname;
  private $birthday;

  }

Z kontrolera chcę zrobić:

$som1 = new People('Paul', 'Smith', '1970-01-01');
$som1->getAge();

Oczywiście mogę przepisać getAge() funkcję wewnątrz mojego bytu, nie jest długa, ale jestem bardzo leniwy i jak już napisałem wszystkie possible datetime- > diff () potrzebuję w powyższej usłudze, nie rozumiem dlaczego nie powinienem ich używać...

NB: moje pytanie nie dotyczy tego, jak wstrzyknąć kontener do mojej jednostki, mogę zrozumieć, dlaczego to nie ma sensu, ale bardziej, jaka jest najlepsza praktyka, aby uniknąć przepisywania tej samej funkcji w różnych jednostkach.

Dziedziczenie wydaje się być złym "dobrym pomysłem", ponieważ mógłbym użyć getAge() wewnątrz class BlogArticle i wątpię, że ta klasa BlogArticle powinna dziedziczyć z ta sama klasa co Klasa ludowa...

Mam nadzieję, że wyraziłem się jasno, ale nie jestem pewien...
Author: Developpeur tunisie, 2014-08-27

4 answers

Jednym z głównych nieporozumień dla wielu programistów jest myślenie, że byty doktryny " są " modelem. To pomyłka.

Wstrzykiwanie usług do jednostek doktrynalnych jest objawem"próby zrobienia czegoś więcej niż przechowywanie danych" do twoich jednostek. Kiedy widzisz, że "anty-wzorzec" najprawdopodobniej naruszasz zasadę"pojedynczej odpowiedzi" w solidnym programowanie.

Symfony nie jest frameworkiem MVC, jest tylko frameworkiem VC. Brak M części. Doctine entities (od teraz będę je nazywać entities, Patrz wyjaśnienie na końcu) to "trwałość danych layer", a nie "warstwa modelu". SF ma wiele rzeczy do widoków, kontrolerów internetowych, kontrolerów poleceń... ale nie pomaga w modelowaniu domen ( http://en.wikipedia.org/wiki/Domain_model ) - nawet warstwa trwałości jest doktryną, a nie Symfonią.

Przezwyciężenie problemu w SF2

Gdy "potrzebujesz" usług w warstwie danych, uruchom alert antypatternowy. Przechowywanie powinno być tylko systemem "put here-get from there". Nic. else.

Aby rozwiązać ten problem, należy wprowadzić usługi do "warstwy logicznej" (modelu) i oddzielić je od "pure storage" (warstwy trwałości danych). zgodnie z zasadą jednej odpowiedzialności, umieść logikę na jednej stronie, umieść gettery i settery do mysql na drugiej.

Rozwiązaniem jest utworzenie brakującej Model warstwy, nieobecnej w Symfony2, i nadanie jej "logiki" obiektów domeny, całkowicie oddzielonych i odsprzęgnięty od warstwy data-persistence, która wie "jak przechowywać" model do bazy danych mysql z doctrine, lub do redis, lub po prostu do pliku tekstowego.

Wszystkie te systemy przechowywania powinny być wymienne, a twoje Model powinny nadal ujawniać te same publiczne metody bez absolutnie żadnych zmian dla konsumenta.

Oto Jak to zrobić:

Krok 1: oddziel model od danych-trwałość

Aby to zrobić, w pakiecie, możesz utworzyć kolejny katalog o nazwie Model na poziomie bundle-root (oprócz tests, DependencyInjection i tak), jak w tym przykładzie gry.

Model powinien być warstwą oddzieloną od warstwy trwałości

  • nazwa Model nie jest obowiązkowa, Symfony nic o niej nie mówi. Możesz wybrać, co chcesz.
  • jeśli twój projekt jest prosty (powiedzmy jeden pakiet), możesz utworzyć ten katalog wewnątrz tego samego pakietu.
  • jeśli twój projekt ma wiele pakietów, możesz rozważyć
    • model podzielony na różne wiązki, lub
    • lub - jak na przykładowym obrazku-użyć ModelBundle, który zawiera wszystkie "obiekty" projektu (bez interfejsów, bez kontrolerów, bez poleceń, tylko logika gry i jej testy). W przykładzie widzimy ModelBundle, dostarczający pojęć logicznych, takich jak Board, Piece lub Tile wśród wielu innych, struktury w katalogach dla jasności.

Szczególnie na twoje pytanie

W Twoim przykład, możesz mieć:

Entity/People.php
Model/People.php
  • wszystko co związane z" przechowywaniem "powinno znaleźć się w Entity/People.php - Ex: Załóżmy, że chcesz przechowywać datę urodzenia zarówno w polu Data-czas, jak i w trzech nadmiarowych polach: Rok, Miesiąc, Dzień, z powodu wszelkich trudnych rzeczy związanych z wyszukiwaniem lub indeksowaniem, które nie są związane z domeną(tzn. nie są związane z "logiką" osoby).
  • wszystko co związane z" logiką " powinno wejść do środka Model/People.php - Ex: Jak obliczyć, czy osoba jest ponad większością wieku właśnie teraz, biorąc pod uwagę pewną datę urodzenia i kraj, w którym mieszka(co określi minimalny wiek). Jak widzicie, to nie ma nic wspólnego z uporem.

Krok 2: Użyj fabryk

Następnie, należy pamiętać, że konsumenci modelu, powinny nigdy przenigdy tworzyć obiekty modelu za pomocą "new". Zamiast tego powinni użyć fabryki, która poprawnie skonfiguruje obiekty modelu(będzie wiązać się z odpowiednią warstwą przechowywania danych). Jedyny wyjątek jest w testach jednostkowych (zobaczymy później). Ale oprócz testów unitarnych, chwyć to ogniem w mózgu i wytatuuj to laserem w siatkówce: nigdy nie rób "nowego" w kontrolerze lub komendzie. Zamiast tego korzystaj z fabryk;)

Aby to zrobić, tworzysz usługę, która działa jako "getter" Twojego modelu. Tworzysz getter jako fabrykę dostępną za pośrednictwem usługi. Zobacz zdjęcie:

Skorzystaj z usługi jako fabryki, aby uzyskać swój model

Możesz zobaczyć Boardmanagera.php tam. To jest fabryka. Informatyka działa jako główny getter dla wszystkiego, co związane z tablicami. W tym przypadku BoardManager posiada następujące metody:

public function createBoardFromScratch( $width, $height )
public function loadBoardFromJson( $document )
public function loadBoardFromTemplate( $boardTemplate )
public function cloneBoard( $referenceBoard )
  • następnie, jak widać na obrazku, w usługach.yml definiujesz tego menedżera i wstawiasz do niego warstwę trwałości. W takim przypadku należy wstrzyknąć ObjectStorageManager do BoardManager. ObjectStorageManager jest, dla tego przykładu, w stanie przechowywać i ładować obiekty z bazy danych lub z pliku; podczas gdy BoardManager jest agnostyczny.
  • Zobacz też ObjectStorageManager na obrazku, który z kolei jest wstrzykiwany @ doctrine, aby móc uzyskać dostęp do mysql.
  • Twoi menedżerowie są jedynym miejscem, gdzie new jest dozwolone. Nigdy w kontrolerze lub komendzie.

Szczególnie na twoje pytanie

W twoim przykładzie, będziesz miał PeopleManager w modelu, zdolnym do uzyskania obiektów people według potrzeb.

Również w modelu powinieneś używać właściwych nazw w liczbie pojedynczej i mnogiej, ponieważ jest to oddzielone od Twojego warstwa trwałości danych. Wygląda na to, że obecnie używasz People do reprezentowania pojedynczego Person - może to być spowodowane tym, że aktualnie (błędnie) dopasowujesz model do nazwy tabeli bazy danych.

Więc, zaangażowane klasy modeli będą:

PeopleManager -> the factory
People -> A collection of persons.
Person -> A single person.

Na przykład (pseudocode! użycie notacji C++ do wskazania typu powrotu):

PeopleManager
{
    // Examples of getting single objects:
    Person getPersonById( $personId ); -> Load it from somewhere (mysql, redis, mongo, file...)
    Person ClonePerson( $referencePerson ); -> Maybe you need or not, depending on the nature the your problem that your program solves.
    Person CreatePersonFromScratch( $name, $lastName, $birthDate ); -> returns a properly initialized person.

    // Examples of getting collections of objects:
    People getPeopleByTown( $townId ); -> returns a collection of people that lives in the given town.
}

People implements ArrayObject
{
    // You could overload assignment, so you can throw an exception if any non-person object is added, so you can always rely on that People contains only Person objects.
}

Person
{
    private $firstname;
    private $lastname;
    private $birthday;
}
Więc, kontynuując swój przykład, kiedy to zrobisz...
// **Never ever** do a new from a controller!!!
$som1 = new People('Paul', 'Smith', '1970-01-01');
$som1->getAge();

...teraz możesz mutować do:

// Use factory services instead:
$peopleManager = $this->get( 'myproject.people.manager' );
$som1 = $peopleManager->createPersonFromScratch( 'Paul', 'Smith', '1970-01-01' );
$som1->getAge();

The PeopleManager will do the Dla Ciebie.

W tym momencie zmienna $som1 typu Person, tak jak została utworzona przez fabrykę, może być wstępnie wypełniona mechaniką niezbędną do przechowywania i zapisywania w warstwie persistence.

myproject.people.manager zostaną zdefiniowane w Twoich usługach.yml i będzie miał dostęp do doktryny albo bezpośrednio, albo poprzez ' myproject.wytrwałość.warstwa menedżera czy coś.

Uwaga: to wstrzyknięcie warstwy trwałości za pośrednictwem Menedżera, ma kilka skutków ubocznych, które boczna ścieżka z "jak sprawić, by model miał dostęp do usług". Zobacz kroki 4 i 5.

Krok 3: wstrzyknij potrzebne usługi za pośrednictwem fabryki.

Teraz możesz wprowadzić wszelkie potrzebne usługi do ludzi.manager

Ty, Jeśli twój obiekt modelu musi uzyskać dostęp do tej usługi, masz teraz 2 opcje:

  • gdy fabryka tworzy obiekt modelu (tj. gdy PeopleManager tworzy osobę), aby wstrzyknąć go za pomocą konstruktora, albo seter.
  • Zastąp funkcję w PeopleManager i wstrzyknij PeopleManager przez konstruktor lub setter.

W tym przykładzie zapewniamy PeopleManager usługę, która będzie wykorzystana przez model. Gdy menedżer osób zostanie poproszony o nowy obiekt modelu, w zdaniu new wstrzykuje mu potrzebną usługę, aby obiekt modelu mógł uzyskać bezpośredni dostęp do zewnętrznej usługi.

// Example of injecting the low-level service.
class PeopleManager
{
    private $externalService = null;

    class PeopleManager( ServiceType $externalService )
    {
        $this->externalService = $externalService;
    }

    public function CreatePersonFromScratch()
    {
        $externalService = $this->externalService;
        $p = new Person( $externalService );
    }
}

class Person
{
    private $externalService = null;

    class Person( ServiceType $externalService )
    {
        $this->externalService = $externalService;
    }

    public function ConsumeTheService()
    {
        $this->externalService->nativeCall();  // Use the external API.
    }
}

// Using it.
$peopleManager = $this->get( 'myproject.people.manager' );
$person = $peopleManager->createPersonFromScratch();
$person->consumeTheService()

W tym przykładzie zapewniamy PeopleManager usługa, która ma być wykorzystana przez modelkę. Niemniej jednak, gdy menedżer osób zostanie poproszony o nowy obiekt modelu, wstrzykuje on sam do utworzonego obiektu, aby obiekt modelu mógł uzyskać dostęp do usługi zewnętrznej za pośrednictwem Menedżera, który następnie ukrywa API, więc jeśli kiedykolwiek usługa zewnętrzna zmieni API, menedżer może wykonać odpowiednie konwersje dla wszystkich konsumentów w modelu.

// Second example. Using the manager as a proxy.
class PeopleManager
{
    private $externalService = null;

    class PeopleManager( ServiceType $externalService )
    {
        $this->externalService = $externalService;
    }

    public function createPersonFromScratch()
    {
        $externalService = $this->externalService;
        $p = new Person( $externalService);
    }

    public function wrapperCall()
    {
         return $this->externalService->nativeCall();
    }
}

class Person
{
    private $peopleManager = null;

    class Person( PeopleManager $peopleManager )
    {
        $this->peopleManager = $peopleManager ;
    }

    public function ConsumeTheService()
    {
        $this->peopleManager->wrapperCall(); // Use the manager to call the external API.
    }
}

// Using it.
$peopleManager = $this->get( 'myproject.people.manager' );
$person = $peopleManager->createPersonFromScratch();
$person->ConsumeTheService()

Krok 4: rzucaj zdarzenia za wszystko

W tym momencie możesz użyć dowolnego serwis w każdym modelu. Wygląda na to, że wszystko jest zrobione.

Niemniej jednak, kiedy go zaimplementujesz, napotkasz problemy z oddzieleniem modelu od jednostki, jeśli chcesz prawdziwie solidnego wzoru. Dotyczy to również oddzielenia tego modelu od innych części modelu.

Problem wyraźnie pojawia się w miejscach takich jak " kiedy zrobić flush () "lub" kiedy zdecydować, czy coś musi być zapisane lub pozostawione do zapisania później " (szczególnie w długotrwałych procesach PHP), jak również problematyczne zmiany w case the doctrine changes its API and things like this.

Ale jest również prawdą, gdy chcesz przetestować osobę bez testowania jej domu, ale dom musi "monitorować", jeśli osoba zmienia swoje imię, aby zmienić imię w skrzynce pocztowej. Jest to szczególnie próba dla długotrwałych procesów.

Rozwiązaniem jest użycie wzorca obserwatora ( http://en.wikipedia.org/wiki/Observer_pattern ) więc twoje obiekty modelowe rzucają zdarzenia prawie za wszystko, a obserwator decyduje się cache danych do pamięci RAM, do wypełnienia danych lub do przechowywania danych na dysku.

To zdecydowanie wzmacnia zasadę stałej / zamkniętej. Nigdy nie należy zmieniać modelu, jeśli zmiana nie jest związana z domeną. Na przykład dodanie nowego sposobu przechowywania do nowego typu bazy danych powinno wymagać edycji zero na klasach modelu.

Możesz zobaczyć przykład tego na poniższym obrazku. W nim podkreślam pakiet o nazwie "TurnBasedBundle", który jest jak podstawowa funkcjonalność dla każda gra turowa, pomimo tego, czy ma planszę, czy nie. Widać, że pakiet zawiera tylko Model i testy.

Każda gra ma zestaw reguł, graczy, a podczas gry, gracze wyrażają pragnienia tego, co chcą zrobić.

W obiekcie Game instancjanci dodają zestaw reguł (poker? szachy? tic-tac-toe?). Uwaga: Co zrobić, jeśli zestaw reguł, który chcę załadować, nie istnieje?

Podczas inicjalizacji ktoś (może kontroler / start) będzie dodaj graczy . Uwaga: co jeśli gra jest 2-graczy i dodam trzy?

I podczas gry kontroler, który odbiera ruchy graczy, doda pragnienia (na przykład, jeśli gra w szachy, "gracz chce przenieść królową na tę płytkę" - co może być ważne lub nie-.

Na zdjęciu widać te 3 akcje pod kontrolą dzięki wydarzeniom.

Przykład rzucania zdarzeń z modelu dla praktycznie wszystkiego, co może się zdarzyć

  • można zauważyć, że pakiet ma tylko Model i testy.
  • w modelu definiujemy nasze 2 obiekty: Game i GameManager, aby uzyskać instancje obiektów Game.
  • definiujemy również interfejsy, jak na przykład GameObserver, więc każdy chętny do odbierania wydarzeń z gry powinien być folkiem GameObserver.
  • Wtedy widać, że dla każdej akcji modyfikującej stan modelu (np. dodanie odtwarzacza), mam 2 zdarzenia: PRE i POST. Zobacz jak to działa:

    1. Ktoś dzwoni metoda $game - >addPlayer ($player).
    2. gdy tylko wejdziemy w funkcję addPlayer (), wywołane zostanie Zdarzenie PRE.
    3. obserwatorzy mogą przechwycić to wydarzenie, aby zdecydować, czy gracz może zostać dodany, czy nie.
    4. wszystkie PRE zdarzenia powinny pochodzić z anulowania przekazanego przez odniesienie. Jeśli więc ktoś zdecyduje, że jest to gra dla 2 graczy i spróbujesz dodać 3rd, $cancel zostanie ustawiony na true.
    5. następnie jesteś ponownie wewnątrz funkcji addPlayer. Możesz sprawdzić, czy ktoś chciał odwołać operację.
    6. wykonaj operację, jeśli jest to dozwolone (ie: mutuj stan $this ->).
    7. po zmianie stanu wywołaj Zdarzenie POST, aby wskazać obserwatorom, że operacja została zakończona.

Na zdjęciu widzisz trzy, ale oczywiście ma o wiele więcej. Z reguły będziesz mieć prawie 2 zdarzenia na seter, 2 zdarzenia na metodę, które mogą modyfikować stan modelu i 1 Zdarzenie dla każdego "nieuniknione" działanie. Więc jeśli masz 10 metod na klasie, które na niej działają, możesz spodziewać się około 15 lub 20 zdarzeń.

Możesz to łatwo zobaczyć w typowym prostym polu tekstowym dowolnej biblioteki graphyc dowolnego systemu operacyjnego: typowe zdarzenia to: gotFocus, lostFocus, keyPress, keyDown, keyUp, mouseDown, mouseMove, itp...

W szczególności, w twoim przykładzie

Osoba będzie miała coś takiego jak preChangeAge, postChangeAge, preChangeName, postChangeName, preChangeLastName, postChangeLastName, jeśli masz ustawiacze dla każdego z nich.

W przypadku długotrwałych działań, takich jak "person, do walk for 10 seconds", możesz mieć 3: preStartWalking, postStartWalking, postStopWalking(w przypadku, gdy nie można programowo zapobiec zatrzymaniu 10 sekund).

Jeśli chcesz uprościć, możesz mieć dwa pojedyncze zdarzenia preChanged( $what, & $cancel ) i postChanged( $what ) za wszystko.

Jeśli nigdy nie zapobiegniesz zmianom, możesz mieć nawet jedną pojedyncze zdarzenie changed() dla wszystkich i każdej zmiany modelu. Następnie Twój obiekt po prostu" skopiuje " właściwości modelu we właściwościach obiektu przy każdej zmianie. Jest to w porządku dla prostych klas i projektów lub struktur, których nie zamierzasz publikować dla klientów innych firm, i oszczędza trochę kodowania. Jeśli Klasa modelu stanie się klasą podstawową w Twoim projekcie, poświęcenie trochę czasu na dodanie listy wszystkich wydarzeń pozwoli Ci zaoszczędzić czas w przyszłości.

Krok 5: przechwytywanie zdarzeń z danych warstwa.

Właśnie w tym momencie Twój Pakiet warstwy danych wchodzi do akcji!!!

Uczyń warstwę danych oberverem modelu. Gdy model zmieni swój stan wewnętrzny, stwórz jednostkę, aby "skopiowała" ten stan do stanu jednostki.

W tym przypadku MVC działa zgodnie z oczekiwaniami: kontroler działa na modelu. Konsekwencje tego są nadal ukryte przed kontrolerem (ponieważ kontroler nie powinien mieć dostępu do doktryny). Model "nadaje" operacja wykonana, więc każdy zainteresowany wie, co z kolei powoduje, że warstwa danych wie o zmianie modelu.

W szczególności w Twoim projekcie

Obiekt Model/Person zostanie utworzony przez PeopleManager. Podczas jego tworzenia, podsystem PeopleManager, który jest usługą, a więc może mieć inne usługi wstrzykiwane, może mieć pod ręką podsystem ObjectStorageManager. Więc {[30] } może uzyskać Entity/People, które odwołujesz w swoim pytaniu i dodać Entity/People jako obserwatora do Model/Person.

W Entity/People głównie zastępujesz wszystkich seterów łapaczami zdarzeń.

Czytasz swój kod w następujący sposób: gdy Model/Person zmieni swoją ostatnią nazwę, Entity/People zostanie powiadomiony i skopiuje dane do swojej wewnętrznej struktury.

Najprawdopodobniej jesteś kuszony, aby wstrzyknąć encję wewnątrz modelu, więc zamiast rzucać Zdarzenie, nazywasz seterami encji.

Ale przy takim podejściu "łamiesz" zasadę otwarto-zamkniętą. Więc jeśli w jakimkolwiek biorąc pod uwagę punkt, w którym chcesz przeprowadzić migrację do MongoDb, musisz "zmienić" swoje "encje" przez "dokumenty" w swoim modelu. W przypadku wzorca obserwatora zmiana ta zachodzi poza Modelem, który nigdy nie zna natury obserwatora poza tym jest jego osobowością.

Krok 6: test jednostkowy wszystko

Wreszcie, chcesz przetestować swoje oprogramowanie. Ponieważ ten wzór wyjaśniłem pokonuje anty-wzór, który odkryłeś, możesz (i powinieneś) przetestować logikę twój model niezależnie od sposobu przechowywania.

Podążanie za tym wzorcem pomaga iść w kierunku stałych zasad, więc każda "jednostka kodu" jest niezależna od pozostałych. Pozwala to na tworzenie testów jednostkowych, które będą testować "logikę" Twojej Model bez zapisywania do bazy danych, ponieważ wprowadzi fałszywą warstwę przechowywania danych jako test-double.

Pozwolę sobie jeszcze raz użyć przykładu gry. Pokazuję wam na obrazku Test gry. Załóżmy, że wszystkie gry mogą trwać kilka dni i DateTime jest przechowywany w bazie danych. W przykładzie obecnie testujemy tylko wtedy, gdy getStartDate () zwraca obiekt dateTime.

Tutaj wpisz opis obrazka

Są w nim strzałki, które reprezentują przepływ.

W tym przykładzie, z dwóch strategii wstrzykiwania, które ci powiedziałem, wybieram pierwszą: aby wstrzyknąć do obiektu modelu Game usługi, których potrzebuje (w tym przypadku BoardManager, PieceManager i ObjectStorageManager) i nie wstrzykiwać GameManager siebie.

  1. najpierw wywołujesz phpunit, który wywoła look dla katalogu Tests, rekurencyjnie we wszystkich katalogach, znajdując klasy o nazwie XxxTest. Następnie będzie chciał wywołać wszystkie metody nazwane textSomething ().
  2. ale przed wywołaniem, dla każdej metody testowej wywołuje setup ().
  3. w konfiguracji stworzymy kilka podwajaczy testowych, aby uniknąć "rzeczywistego dostępu" do bazy danych podczas testowania, podczas prawidłowego testowania logiki w naszym modelu. W tym przypadku podwójny mojego własnego menedżera warstw danych, ObjectStorageManager.
  4. jest przypisana do zmiennej tymczasowej dla jasności...
  5. ...który jest przechowywany w instancji GameTest...
  6. ...do późniejszego wykorzystania w samym teście.
  7. zmienna $sut (system under test) jest następnie tworzona za pomocą polecenia new, a nie za pomocą Menedżera. Pamiętasz, że mówiłem, że testy są wyjątkiem? Jeśli używasz menedżera (nadal możesz) tutaj nie jest to test jednostkowy, tylko test integracyjny, ponieważ testuje dwa zajęcia: menadżer i gra. W Komendzie new fałszujemy wszystkie zależności, które ma model(jak menedżer planszy i jak menedżer elementów). Jestem hardcoding GameId = 1 tutaj. Dotyczy to trwałości danych, patrz poniżej.
  8. możemy następnie wywołać testowany system (prosty obiekt modelu Game), aby przetestować jego wewnętrzne elementy.

Jestem hardcoding "Game id = 1" w new. W tym przypadku testujemy tylko, czy zwracany typ jest obiektem DateTime. Ale na wypadek, gdybyśmy chcemy również sprawdzić, czy otrzymana Data jest właściwa, możemy "dostroić" Obiektstoragemanager (data-persistance layer) Mock, aby zwrócić to, co chcemy w wywołaniu wewnętrznym, więc możemy przetestować, że na przykład, gdy żądam daty do warstwy danych dla game=1 Data to 1st-jun-2014, A Dla game=2 data to 2nd-jun-2014. Następnie w testGetStartDate chciałbym utworzyć 2 nowe instancje, z Ids 1 i 2 i sprawdzić zawartość wyniku.

Szczególnie w Twoim projekt

Będziesz miał Test/Model/PersonTest test jednostkowy, który będzie w stanie grać z logiką osoby, a w przypadku potrzeby osoby z bazy danych, będziesz udawał ją przez makietę.

W przypadku, gdy chcesz przetestować przechowywanie danej osoby w bazie danych, wystarczy, że unit-testujesz, że zdarzenie zostanie wyrzucone, bez względu na to, kto go słucha. Możesz utworzyć fałszywy słuchacz, dołączyć do zdarzenia, a gdy postChangeAge happens oznaczyć flagę i nic nie zrobić (brak prawdziwej bazy danych przechowywanie). Następnie twierdzisz, że flaga jest ustawiona.

W skrócie:

  1. nie należy mylić logiki z trwałością danych. Stwórz Model, który nie ma nic wspólnego z bytami i umieść w nim wszystkie logiki.
  2. nigdy nie używaj new, aby uzyskać modele od jakiegokolwiek konsumenta. Zamiast tego korzystaj z usług fabrycznych. Szczególną uwagę należy unikać wiadomości w kontrolerach i poleceniach. Execption: unit-test jest jedynym konsumentem, który może użyć new
  3. potrzebujesz w modelu za pośrednictwem fabryki, która z kolei otrzymuje go z usług.plik konfiguracyjny yml.
  4. rzucaj wydarzenia za wszystko. Kiedy mówię wszystko, znaczy wszystko. Wyobraź sobie, że obserwujesz model. Co chciałbyś wiedzieć? Dodaj wydarzenie dla niego.
  5. przechwytywanie zdarzeń z kontrolerów, widoków, poleceń i innych części modelu, ale szczególnie przechwytywanie ich w warstwie data-storage, dzięki czemu można" skopiować " obiekt na dysk bez ingerencji w model.
  6. jednostkowy test logiki bez zależności od żadnej prawdziwej bazy danych. Dołącz rzeczywisty system przechowywania baz danych w produkcji i dołącz fałszywą implementację do testów.
Wygląda na to, że to dużo pracy. Ale tak nie jest. To kwestia przyzwyczajenia. Po prostu pomyśl o "obiektach", których potrzebujesz, utwórz je i spraw, aby warstwa danych była "monitorowana" Twoich obiektów. Wtedy twoje obiekty są wolne do uruchomienia, odsprzęgnięte. Jeśli tworzysz model z fabryki, wstrzyknij wszelkie potrzebne usługi w modelu do modelu, i zostawić dane w spokoju.

Edytuj Kwiecień/2016

Wszystkie wystąpienia słowa entityw tej odpowiedzi odnoszą się do "Bytów doktrynalnych", co powoduje zamieszanie u większości programistów, pomiędzy warstwą modeluA warstwą persistance, która powinna być zawsze inna.

  • Doktryna jest infrastrukturą, więc doktryna jest poza modelem przez definicja.
  • Doktryna ma byty . Tak więc, z definicji, to byty doktryny są również poza modelem .
  • zamiast tego rosnąca popularność DDD klocków sprawia, że potrzeba wyjaśnienia jeszcze bardziej mojej odpowiedzi, ponieważ {74]} używa słowa {76]} również w modelu.
  • Domain entities (Nie Doctrine entities) są podobne do tego, o czym mówię w tej odpowiedzi na Domain objects.
  • w rzeczywistości istnieje wiele rodzajów Domain objects:
    • Domain entities (inny z Doctrine entites).
    • Domain value objects (można myśleć podobnie do podstawowych typów, z logiką).
    • Domain events (również różni się od tych Symfony events i również różni się od Doctrine events).
    • Domain commands (różni się od tych Symfony command line pomocników podobnych do kontrolerów).
    • Domain services (inny niż Symfony framework services).
    • itd.
  • dlatego przyjmij moje wyjaśnienie tak: Kiedy mówię "byty nie są obiektami modelowymi", po prostu przeczytaj " byty doktryny nie są domeną podmioty".
Mam nadzieję pomóc.

Xavi.

 68
Author: Xavi Montero,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-10-29 01:11:12

Wspomniałeś już o bardzo dobrym punkcie. Instancje klasy Person nie są jedyną rzeczą, która może mieć wiek. BlogArticles może również starzeć się wraz z wieloma innymi typami. Jeśli używasz PHP 5.4+ , możesz użyć cech, aby dodać małe elementy funkcjonalności zamiast obiektów usługowych z kontenera (lub może możesz je połączyć).

Oto krótka makieta tego, co możesz zrobić, aby uczynić go bardzo elastycznym. To jest podstawowa idea:

  • mają jedną cechę obliczającą wiek (Aging)
  • mają określoną cechę, która może zwrócić odpowiednie pole ($birthdate, $createdDate, ...)
  • użyj cech wewnątrz klasy

Ogólne

trait Aging {
    public function getAge() { 
        return $this->calculate($this->start()); 
    }

    public function calculate($startDate) { ... }
}

Dla osoby

trait AgingPerson {
    use Aging;
    public function start() {
        return $this->birthDate;
    }   
}

class Person {
    use AgingPerson;
    private $birthDate = '1999-01-01'; 
}

Do artykułu na blogu

// Use for articles, pages, news items, ...
trait AgingContent {
    use Aging;
    public function start() {
        return $this->createdDate;
    }   
}

class BlogArticle {
    use AgingContent;
    private $createDate = '2014-01-01'; 
}

Teraz możesz zapytać każdą instancję powyższych klas o ich wiek.

echo (new Person())->getAge();
echo (new BlogArticle())->getAge();

Wreszcie

Jeśli potrzebujesz typu hinting cechy nie pomogą Ci. W takim przypadku będziesz musiał dostarczyć interfejs i niech każda klasa, która używa trait ją zaimplementuje(rzeczywistą implementacją jest trait, ale interfejs umożliwia Typ hinting).

interface Ageable {
    public function getAge();
}

class Person implements Ageable { ... }
class BlogArticle implements Ageable { ... }

function doSomethingWithAgeable(Ageable $object) { ... }

Może to wydawać się dużym kłopotem, gdy w rzeczywistości jest o wiele łatwiejsze do utrzymania i rozszerzenia w ten sposób.

 2
Author: Bart,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-08-26 23:42:51

Duża część polega na tym, że nie ma łatwego sposobu wprowadzania zależności podczas korzystania z bazy danych.

$person = $personRepository->find(1); // How to get the age service injected?

Jednym z rozwiązań może być podanie usługi wieku jako argumentu.

$ageCalculator = $container('age_service');

$person = $personRepository->find(1);

$age = $person->calcAge($ageCalculator);

Ale tak naprawdę, prawdopodobnie lepiej byłoby po prostu dodać rzeczy wieku do swojej klasy osoby. Łatwiejsze do przetestowania i tak dalej.

Wygląda na to, że masz jakieś formatowanie wyjściowe? Takie rzeczy powinny być robione w gałązce. getAge powinien naprawdę zwrócić numer.

Podobnie, twoja data urodzenia powinna być obiektem daty, a nie ciągiem znaków.

 1
Author: Cerad,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-08-26 22:45:57

Masz rację, to generalnie zniechęca. Istnieje jednak kilka podejść, w jaki sposób można rozszerzyć funkcjonalność jednostki poza cel kontenera danych. Oczywiście, wszystkie z nich można uznać (mniej więcej) za złe praktyki ... ale jakoś trzeba wykonać robotę, prawda?

  1. Można rzeczywiście utworzyć AbstractEntity Super klasę, z której dziedziczą wszystkie inne byty. Ta abstrakcja zawierałaby metody pomocnicze, których mogą potrzebować inne podmioty.

  2. Ty może pracować z niestandardowymi repozytoriami doktryn , jeśli potrzebujesz kontekstu encji do pracy z menedżerem encji i zwracasz "bardziej specjalne" wyniki niż te, które dałyby ci zwykłe gettery. Ponieważ masz dostęp do menedżera jednostek w repozytorium, możesz wykonywać wszelkiego rodzaju zapytania specjalne.

  3. Możesz napisać usługę, która jest odpowiedzialna za dany podmiot / podmioty. Minusy: nie możesz kontrolować, czy inne części kodu (lub inni deweloperzy) wiedzą o tym obsługa. Zaleta: nie ma ograniczeń co do tego, co możesz zrobić, a wszystko jest ładnie zamknięte.

  4. Możesz pracować z zdarzenia cyklu życia/wywołania zwrotne.

  5. Jeśli naprawdę potrzebujesz wprowadzić usługi do jednostek, możesz rozważyć ustawienie statycznej właściwości na jednostce i ustawić ją tylko raz w kontrolerze lub dedykowanej usłudze. Wtedy nie musisz dbać o każdą inicjalizację obiektu. Można połączyć z abstrakcją podejdźcie.

Jak wspomniano wcześniej, wszystkie z nich mają swoje zalety i wady. Wybierz truciznę.

 0
Author: lxg,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-08-26 22:39:57