• Najnowsze pytania
  • Bez odpowiedzi
  • Zadaj pytanie
  • Kategorie
  • Tagi
  • Zdobyte punkty
  • Ekipa ninja
  • IRC
  • FAQ
  • Regulamin
  • Książki warte uwagi

Symfony czy w serwisach można rzucać wyjątkami

VPS Starter Arubacloud
0 głosów
524 wizyt
pytanie zadane 1 lutego 2019 w PHP przez `Krzychuu Stary wyjadacz (13,940 p.)
edycja 1 lutego 2019 przez `Krzychuu

Witam

Czy mogę tworzyć wyjątki w klasie serwisu? np. 

throw new HttpException(400, "Nazwa zajęta!");

Załóżmy że dodaje rekord do bazy danych w serwisie sprawdzam czy istnieje rekord o takiej samej nazwie jeżeli istnieje to lepiej będzie wyrzucić wyjątek taki jak wyżej czy zwrócić do kontrolera błąd i tam zwrócić nazwę i kod błędu?

3 odpowiedzi

+2 głosów
odpowiedź 1 lutego 2019 przez Comandeer Guru (599,730 p.)
wybrane 2 lutego 2019 przez `Krzychuu
 
Najlepsza

Usługa raczej powinna rzucać wyjątki dotyczące tego co robi, np. brak połączenia z bazą – wyjątek NoDBConnectionException lub podobny. Warstwa HTTP to nie jest coś, czym powinna się zajmować usługa.

Dodatkowo wyjątki powinny dotyczyć – jak sama nazwa sugeruje – sytuacji wyjątkowych. Owszem, można wszelkie błędy obsługiwać przy pomocy wyjątków, ale wówczas w kontrolerze narośnie nam wiele bloków try/catch obsługujących poszczególne rzeczy. W tym wypadku zwróciłbym po prostu informację, że rekord nie został dodany, z opcjonalną informacją czemu. Innymi słowy: wyjątki zostawiłbym dla błędów niezależnych od naszego kodu (brak połączenia z bazą, niemożność zapisu do pliku itd.), a błędy związane bezpośrednio z działaniem aplikacji czy błędnie wprowadzonymi przez użytkownika danymi (a zatem błędami, o których użytkownik powinien się dowiedzieć) obsługiwałbym "normalnie".

Ogólnie to widziałbym to jakoś tak:

<?php
public function handleRequest( $req ) {
	try {
		if ( !$this->myService->saveToDB( $req ) ) {
			// Nie zapisano, trzeba wyświetlić błąd użytkownikowi.
		}
	} catch( Exception $e ) {
		// Baza się krzakła.
		throw new HTTPException( 500 );
	}
}

W sumie tutaj jest fajny artykuł opisujący dokładniej to, o czym napisałem: http://adam.wroclaw.pl/2015/05/wyjatki-kiedy-jak-i-po-co/

Oczywiście można też stosować podejście zupełnie odwrotne: https://zawarstwaabstrakcji.pl/20170309-walidacja-w-architekturze-wielowarstwowej/ – pytanie, na ile chcemy komplikować całość.

1
komentarz 1 lutego 2019 przez HaKIM Szeryf (87,590 p.)

Przechwytywanie wyjątków i zamiana ich na "JsonResponse" wraz z wiadomością co poszło nie tak współgra znakomicie z Ajaxem.

Przykład:

throw NoAmmoException::withMessage("No ammo left");

potem handlujemy w kontrolerze czy gdzie lubimy:

try {
    $gun->shoot();
} catch (GunLogicException $logicExceptiopn) {
    // Zwrotka Response z opdowiednim kodem + message
}

a w Ajax:

$.ajax({
    [...]
    statusCode: {
        200: function () {
            [...]
        },
        400: function () {
            alert(response.message);
        },
        406: function () {
            alert(response.message);
        }
    }
});

Ofc. zamiast alert jakiś fajny message ustawiamy na div.id."failed" czy coś. :p

Co myślicie o tym podejściu?

komentarz 1 lutego 2019 przez `Krzychuu Stary wyjadacz (13,940 p.)
dziękuje chętnie poczytam, cała sytuacja z wyjątkami wyszła z tego, że źle do całej sprawy podszedłem, chciałem zrobić jeden serwis który dodaje przepis do bazy danych i od razu by sprawdzał w tej samej metodzie czy taki przepis istnieje, po dłuższym zastanowieniu doszedłem do wniosku że powinienem rozbić metodę która sprawdza czy przepis już istnieje i zrobić oddzielny serwis dla niej, chyba że jeszcze inaczej powinienem to zrobić.
1
komentarz 1 lutego 2019 przez HaKIM Szeryf (87,590 p.)

A nie lepiej Ci po prostu zrobić metodę w repository:

use Entity\Exception\Recipe\RecipeNotFoundException;

class RecipesRepository // extends Foo implements Bar
{
    // [...]
    
    public function get(string $id): Recipe
    {
        $result = []; // Jakiś queryBuilder z zapytaniem
        
        if (!$result) {
            throw new RecipeNotFoundException();
        }
        
        return $result;
    }
    
    // [...]
}

I potem w kontrolerze sobie obsłużysz ten wyjątek, a dokładniej NotFoundException, co byś miał na większość takich przypadków.

W patternie Repository get różni się od find tym, że find może być nullable a get musi zwrócić wartość albo wypluwa wyjątek.

komentarz 1 lutego 2019 przez `Krzychuu Stary wyjadacz (13,940 p.)
w sumie tak, zawsze komplikuje sprawę :P
1
komentarz 1 lutego 2019 przez Ehlert Ekspert (212,630 p.)
Po co sprawdzać czy coś istnieje skoro mamy constraint UniqueEntity.
1
komentarz 1 lutego 2019 przez HaKIM Szeryf (87,590 p.)

Zapomniałem, że Krzychu chce tego użyć do sprawdzenia czy Recipe istnieje przy próbie stworzenia nowego.

Ehlert ma racje, nie potrzebujesz żadnej dodatkowej metody do tego use case'a.

Wystarczy handlnąć:

Doctrine\DBAL\Exception\UniqueConstraintViolationException;
komentarz 1 lutego 2019 przez `Krzychuu Stary wyjadacz (13,940 p.)

@Ehlert, dzięki, to działa tak jak zwykła assert, że przy sprawdzaniu czy $form->isValid() wywali błąd?, bo chciałbym przesłać do frontu ta informację i wyświetlić przy polu z nazwą

komentarz 1 lutego 2019 przez Comandeer Guru (599,730 p.)
Tylko że tego typu constraint odpali się dopiero na poziomie encji, przy próbie zapisu całości do bazy. IMO z punktu widzenia UX byłoby fajnie, gdyby walidacja tego typu rzeczy przebiegała na wyższym poziomie, a nie na poziomie po prostu dbającym o integralność danych.

Na dobrą sprawę kontroler nie powinien znać szczegółów implementacyjnych usługi, więc obsłużenie w nim wyjątku rzuconego bezpośrednio przez DBAL jest IMO złamaniem granicy warstw.
komentarz 1 lutego 2019 przez `Krzychuu Stary wyjadacz (13,940 p.)
to co w tej sytuacji powinienem zrobić?
komentarz 1 lutego 2019 przez HaKIM Szeryf (87,590 p.)
edycja 1 lutego 2019 przez HaKIM

Mógłbyś w warstwie infrastruktury (czyt. w repo) przechwycić ten wyjątek i wypluć własny.

try {
    $this->entityManager->add($recipe);
    $this->entityManager->flush();
} catch (Unique... $e) {
    throw  RecipeAlreadyExists::withMessage("Lorem ipsum");
}

Wtedy powinno się zgadzać, bo Twój przepływ między warstwami pozostanie jednokierunkowy[1] (zakłdając, że korzystanie z bibliotek to taki skok w bok). Choć, nie jestem przekonany czy ma to aż tak duże znacznie - Doctrine tak szybko nie wymienisz. :p

[1] - O ile jest to pożądane w wybranej przez nas architekturze.

Nadal, nie wiem co Comandeer miał na myśli przez:

[...] obsłużenie w nim wyjątku rzuconego bezpośrednio przez DBAL jest IMO złamaniem granicy warstw.

Kiedy dochodzi do złamania granicy warstw?

Nie potrafię sobie wyobrazić apki, gdzie każda warstwa może jedynie odnosić się do warstwy poniżej. Na moje oko wprowadziłoby to za dużo kosmetycznej abstrakcji.

komentarz 1 lutego 2019 przez Comandeer Guru (599,730 p.)

Raczej warstwa wyżej nie odnosi się do warstwy niżej. Z punktu widzenia logiki aplikacji fakt, że baza danych jest obsługiwana przez Doctrine stanowi szczegół implementacyjny, nic więcej. Dywagowałbym, czy fakt, że encja zwróciła ten a nie inny wyjątek powinien obchodzić warstwę wyżej. Z jej punktu widzenia nie udało się zapisać danych – tyle.

Jak dla mnie fakt, że encja rzuca wyjątkiem, że taki rekord już istnieje, służyć ma wyłącznie zachowaniu integralności danych i nie jest w żaden sposób przeznaczony dla użytkownika. Dla użytkownika natomiast powinna być przeprowadzana wcześniej walidacja, która moim zdaniem może odpytać bazę, czy taki rekord istnieje. Użytkownik nie powinien np. wybierać nazwy użytkownika i przechodzić całego procesu rejestracji tylko po to, by na końcu okazało się, że dany nick jest zajęty i musi jeszcze raz wypełniać/poprawiać formularz.

Zatem:

  1. Walidacja danych przesłanych przez użytkownika i tutaj walidator zwraca false dla niepoprawnych danych + może zwrócić dodatkowe informacje, co dokładnie jest źle.
  2. Jeśli dane są zwalidowane, dane trafiają do warstwy persystencji, która próbuje je zapisać. Tutaj zachodzi wewnętrzna walidacja (jak się dobrze rozegra system, to może być realizowana przy pomocy tych samych reguł co walidacja wyżej) i jeśli dane nie są poprawne lub rozsadzą integralność bazy (vide omawiany przypadek) – leci wyjątek, który warstwa wyżej odczytuje jako niemożność zapisania danych.

Opieranie wszystkiego na wyjątkach brzmi jak budowanie kontroli przepływu przy pomocy wyjątków – a to niekoniecznie jest dobra praktyka.

komentarz 2 lutego 2019 przez Ehlert Ekspert (212,630 p.)

Opieranie wszystkiego na wyjątkach brzmi jak budowanie kontroli przepływu przy pomocy wyjątków – a to niekoniecznie jest dobra praktyka.

Z tym się zgadzam. Takie podejście było w Symfony 1 przy walidacji formularzy.

Tylko że tego typu constraint odpali się dopiero na poziomie encji, przy próbie zapisu całości do bazy. 

Z tym się nie zgadzam. Constraint jak każdy inny odpali się na poziomie walidacji formularza, lub encji.

komentarz 2 lutego 2019 przez HaKIM Szeryf (87,590 p.)

Nie potrafię sobie wyobrazić apki, gdzie każda warstwa może jedynie odnosić się do warstwy poniżej. Na moje oko wprowadziłoby to za dużo kosmetycznej abstrakcji.

- HaKIM

Raczej warstwa wyżej nie odnosi się do warstwy niżej.

- Comandeer

Niedokładnie się wyraziłem.

- Nie potrafię sobie wyobrazić apki, gdzie każda z warstw może jedynie odnieść się do warstwy pod nią. T.j. UI tylko może odnieść się do Application i łapy precz od Infra czy Domain.

Zakładając, że odniesienie się do warstwy dwa poziomy niżej jest przekroczeniem granicy.

Opieranie wszystkiego na wyjątkach brzmi jak budowanie kontroli przepływu przy pomocy wyjątków – a to niekoniecznie jest dobra praktyka.

- Comandeer 

  1. Walidacja danych przesłanych przez użytkownika i tutaj walidator zwraca false dla niepoprawnych danych + może zwrócić dodatkowe informacje, co dokładnie jest źle.

- Comandeer

Mi to brzmi jak wyjątek albo przynajmniej boolean na steroidach.

No i pozostaje kwestia rozkraczenia business rules na warstwę Application.

Choć, ja to trochę mówię o innym przypadku walidowania niżeli autor tematu. Nie korzystam z form validatora. Ajax + "Exception API". cheeky

komentarz 2 lutego 2019 przez `Krzychuu Stary wyjadacz (13,940 p.)
jaka jest najlepsza metoda do przesyłania kodu z błędem do frontu, tzn chodzi mi o to że po stronie serwera jest błąd który powiadamia że taka nazwa już istnieje i jak teraz w dobry sposób przesłać to do frontu żeby we froncie widzieć że to chodzi akurat o istniejącą nazwę, przypuszczając że mogą też występować inne błędy.
komentarz 2 lutego 2019 przez HaKIM Szeryf (87,590 p.)

Nie ma najlepszej. Wszystko zależy od projektu, języka, frameworka lub jego braku, architektury, czasu (deadline) i masy innych czynników.

Powiedziawszy to możesz zajrzeć do:

https://symfony.com/doc/current/validation.html

I odpowiedzieć sobie na pytanie czy jest to dobre rozwiązanie dla Twojego projektu.

komentarz 2 lutego 2019 przez `Krzychuu Stary wyjadacz (13,940 p.)
Tzn chodzi mi bardziej jaki response zwrócić, żeby odczytać że konkretnie o ten błąd chodzi.
komentarz 2 lutego 2019 przez HaKIM Szeryf (87,590 p.)

Jeśli chodzi o konflikt dwóch takich samych rekordów to (w Symfony):

$message = $exception->getMessage() ?? 'Something went wrong';

return new Response($message, Response::HTTP_CONFLICT);

I sprawdzasz np. Ajax zwracany status code.

Ale z tego co widziałem to Ty korzystasz z walidatora Symfony, a tam to trochę inaczej wygląda.

Odsyłam do dokumentacji: https://symfony.com/doc/current/validation.html

komentarz 2 lutego 2019 przez `Krzychuu Stary wyjadacz (13,940 p.)
Dziękuje za pomoc :)
komentarz 2 lutego 2019 przez Comandeer Guru (599,730 p.)

Nie potrafię sobie wyobrazić apki, gdzie każda z warstw może jedynie odnieść się do warstwy pod nią. T.j. UI tylko może odnieść się do Application i łapy precz od Infra czy Domain.

Warstwy wgl nie powinny się odnosić do siebie. Jedna warstwa przekazuje innej warstwie dane i czeka na wynik – cała interakcja.

Mi to brzmi jak wyjątek albo przynajmniej boolean na steroidach.

if ($validator->validate($data) ) {
    var_dump($validator->getErrors());
}

 

+1 głos
odpowiedź 1 lutego 2019 przez ShiroUmizake Nałogowiec (46,300 p.)
Ja bym zrobił zwrotkę do kontrolera i tam obsłużył wyjątek. Bo potem się zakopiesz w try/catch :P
komentarz 1 lutego 2019 przez `Krzychuu Stary wyjadacz (13,940 p.)
W Symfony można rzucać wyjątkami bez try/catch
1
komentarz 1 lutego 2019 przez HaKIM Szeryf (87,590 p.)

W Symfony można rzucać wyjątkami bez try/catch

Co z tego? Wszędzie możesz rzucać bez try catch. Try catch nie ma nic do rzucania wyjątkami, tylko ich przechwytywania.

Imo. głos w dół niezasłużony, bo ShiroUmizake podpowiedział b. dobrze.

1
komentarz 1 lutego 2019 przez `Krzychuu Stary wyjadacz (13,940 p.)
Szczerze to o tym nie widziałem, myślałem nad napisaniem event lisntener do przechwytywania błędu.

Ps. Głos w dół nie był ode mnie
1
komentarz 1 lutego 2019 przez HaKIM Szeryf (87,590 p.)
Ja sobie tak to załatwiłem u siebie:

https://github.com/HaKIMus/smartphones/blob/develop/src/Controller/Api/Smartphones/SmartphonesApi.php

Jestem bardzo dumny z tego kodzika, bo wszystkie try i catch wyizolowałem do handlera. :)

A tu sam exception handler:

https://github.com/HaKIMus/smartphones/blob/develop/src/Controller/Api/Handlers/SmartphonesApiHandler.php

W następnej wersji powinienem dodać do niego abstrakcje, po której będą rozszerzały wszystkie controller handlery aby uzyskać dostęp do chronionych metod t.j. returnResponse itp.
komentarz 1 lutego 2019 przez `Krzychuu Stary wyjadacz (13,940 p.)
Fajnie to wygląda, jakbym miał napisany handler to wtedy nie byłoby problemu z używaniem wyjątków w serwisach?, Rzucanie wyjątków można zaliczyć do logiki czy takie sprawy bardziej powinny być w kontrolerze ?
1
komentarz 1 lutego 2019 przez HaKIM Szeryf (87,590 p.)

Nie ma żadnego problemu z używaniem wyjątków gdziekolwiek, gdzie jest to potrzebne. Grunt, żeby je gdzieś obsłużyć, bo inaczej User dostanie Internal 500 na twarz a w gorszym wypadku, przy źle skonfigurowanej produkcji, exception ze wszystkimi informacjami.

Rzucanie wyjątków można zaliczyć do logiki czy takie sprawy bardziej powinny być w kontrolerze ?

Jest to logika, ale to tak jakbyś się czepiał ifów czy loopów w templatkach. Koszt wyizolowania tych rzeczy jest większy, niżeli trzymanie tego w kontrolerze.

komentarz 1 lutego 2019 przez `Krzychuu Stary wyjadacz (13,940 p.)
Dziękuje za pomoc i cenne informacje :)
1
komentarz 1 lutego 2019 przez HaKIM Szeryf (87,590 p.)

Tak nawiasem mówiąc to w tym kodzie:

throw new HttpException(400, "Nazwa zajęta!");

nadaje się prędzej 409 statusik.

No i ja polecam Ci korzystać z Response::HTTP_* - Jeśli lookniesz wgłąb klasy Response to zauważysz, że mają oni tam listę constów ze http statusami.

class Response
{
    const HTTP_CONTINUE = 100;
    const HTTP_SWITCHING_PROTOCOLS = 101;
    const HTTP_PROCESSING = 102;            // RFC2518
    const HTTP_EARLY_HINTS = 103;           // RFC8297
    const HTTP_OK = 200;
    const HTTP_CREATED = 201;
    const HTTP_ACCEPTED = 202;
    const HTTP_NON_AUTHORITATIVE_INFORMATION = 203;
    const HTTP_NO_CONTENT = 204;
    const HTTP_RESET_CONTENT = 205;
    const HTTP_PARTIAL_CONTENT = 206;
    const HTTP_MULTI_STATUS = 207;          // RFC4918
    const HTTP_ALREADY_REPORTED = 208;      // RFC5842
    const HTTP_IM_USED = 226;               // RFC3229
    const HTTP_MULTIPLE_CHOICES = 300;
    const HTTP_MOVED_PERMANENTLY = 301;
    const HTTP_FOUND = 302;
    const HTTP_SEE_OTHER = 303;
    const HTTP_NOT_MODIFIED = 304;
    const HTTP_USE_PROXY = 305;
    const HTTP_RESERVED = 306;
    const HTTP_TEMPORARY_REDIRECT = 307;
    const HTTP_PERMANENTLY_REDIRECT = 308;  // RFC7238
    const HTTP_BAD_REQUEST = 400;
    const HTTP_UNAUTHORIZED = 401;
    const HTTP_PAYMENT_REQUIRED = 402;
    const HTTP_FORBIDDEN = 403;
    const HTTP_NOT_FOUND = 404;
    const HTTP_METHOD_NOT_ALLOWED = 405;
    const HTTP_NOT_ACCEPTABLE = 406;
    const HTTP_PROXY_AUTHENTICATION_REQUIRED = 407;
    const HTTP_REQUEST_TIMEOUT = 408;
    const HTTP_CONFLICT = 409;
    const HTTP_GONE = 410;
    const HTTP_LENGTH_REQUIRED = 411;
    const HTTP_PRECONDITION_FAILED = 412;
    const HTTP_REQUEST_ENTITY_TOO_LARGE = 413;
    const HTTP_REQUEST_URI_TOO_LONG = 414;
    const HTTP_UNSUPPORTED_MEDIA_TYPE = 415;
    const HTTP_REQUESTED_RANGE_NOT_SATISFIABLE = 416;
    const HTTP_EXPECTATION_FAILED = 417;
    const HTTP_I_AM_A_TEAPOT = 418;                                               // RFC2324
    const HTTP_MISDIRECTED_REQUEST = 421;                                         // RFC7540
    const HTTP_UNPROCESSABLE_ENTITY = 422;                                        // RFC4918
    const HTTP_LOCKED = 423;                                                      // RFC4918
    const HTTP_FAILED_DEPENDENCY = 424;                                           // RFC4918

    [...]
}

https://tools.ietf.org/html/draft-ietf-httpbis-p2-semantics-18#section-7.4.10

komentarz 1 lutego 2019 przez `Krzychuu Stary wyjadacz (13,940 p.)
Dziękuje, faktycznie lepiej to będzie wyglądać
+1 głos
odpowiedź 1 lutego 2019 przez Ehlert Ekspert (212,630 p.)
Osobiście nie aż tak często plącze się w try catch, a wyjątkami się rzuca. Kluczowe mogą być dla Ciebie: EventSubscriber oraz event Kernel.EXCEPTION

Podobne pytania

0 głosów
1 odpowiedź 137 wizyt
pytanie zadane 8 marca 2021 w PHP przez michal_php Stary wyjadacz (13,700 p.)
0 głosów
1 odpowiedź 88 wizyt
pytanie zadane 30 września 2020 w PHP przez User007 Bywalec (2,400 p.)
0 głosów
2 odpowiedzi 151 wizyt
pytanie zadane 3 września 2018 w PHP przez BetBet Użytkownik (550 p.)

92,453 zapytań

141,262 odpowiedzi

319,088 komentarzy

61,854 pasjonatów

Motyw:

Akcja Pajacyk

Pajacyk od wielu lat dożywia dzieci. Pomóż klikając w zielony brzuszek na stronie. Dziękujemy! ♡

Oto polecana książka warta uwagi.
Pełną listę książek znajdziesz tutaj.

Akademia Sekuraka

Akademia Sekuraka 2024 zapewnia dostęp do minimum 15 szkoleń online z bezpieczeństwa IT oraz dostęp także do materiałów z edycji Sekurak Academy z roku 2023!

Przy zakupie możecie skorzystać z kodu: pasja-akademia - użyjcie go w koszyku, a uzyskacie rabat -30% na bilety w wersji "Standard"! Więcej informacji na temat akademii 2024 znajdziecie tutaj. Dziękujemy ekipie Sekuraka za taką fajną zniżkę dla wszystkich Pasjonatów!

Akademia Sekuraka

Niedawno wystartował dodruk tej świetnej, rozchwytywanej książki (około 940 stron). Mamy dla Was kod: pasja (wpiszcie go w koszyku), dzięki któremu otrzymujemy 10% zniżki - dziękujemy zaprzyjaźnionej ekipie Sekuraka za taki bonus dla Pasjonatów! Książka to pierwszy tom z serii o ITsec, który łagodnie wprowadzi w świat bezpieczeństwa IT każdą osobę - warto, polecamy!

...