Architektura projektu PHP

0

Pracuje nad jedym ze starszych projektow w yii2. Wszystko jest tam pomieszane, php z html i css i js. SQL query sa wszedzie, powtarzaj sie te same zapytanie. Duzo logiki biznesowej znajduje sie w widokach czy kontrollerach. Do tego masa metod jest statyczne. Generalnie, kod jest nie testowalny. Zamias uzyc Dependency Injection, ktorego w ogole nie zostal uzyty to wszedzie te ochydne statyczne metody. Kod nie czytelny, zero dobrych praktych tj SOLID, zero wzorcow projektowych itd.

Chce troche poprawic ten projekt :) Zaczynajac od nowej architektury. Czyli Np trzymac wszytkie zapytania w jednym miejscu np w klasach repository. Business logic moge trymac np w klasach services itd itd. Nie chce za bardzo uzywac hexagon architecture czy DDD.

Czy moze ktos mial podobnie? Jaka architekture czy wzorce polecacie? Oczywiscie zostaje przy MVC, ale chce usunac logice biznesowac z widokow oraz kontrolerow i ja zcentralizowac w services. Zrobic light controllers. Jakis SOLID zaimplementowac etc. I na tych nowych klasach napisac test jednostowe i troche integracyjnych.

Co byscie jeszcze dodali?

1
  1. Testy e2e (główne scenariusze) - pokryjesz testami, a później przepisuj :)
  2. CQRS
0
greendev napisał(a):
  1. Testy e2e (główne scenariusze) - pokryjesz testami, a później przepisuj :)
  2. CQRS

Co polecasz na test E2E? Codeception?

0

Kiedyś używałem połączenia Yii + Codeception. Było spoko :)

1
poniatowski napisał(a):

Pracuje nad jedym ze starszych projektow w yii2. Wszystko jest tam pomieszane, php z html i css i js. SQL query sa wszedzie, powtarzaj sie te same zapytanie. Duzo logiki biznesowej znajduje sie w widokach czy kontrollerach. Do tego masa metod jest statyczne. Generalnie, kod jest nie testowalny. Zamias uzyc Dependency Injection, ktorego w ogole nie zostal uzyty to wszedzie te ochydne statyczne metody. Kod nie czytelny, zero dobrych praktych tj SOLID, zero wzorcow projektowych itd.

Chce troche poprawic ten projekt :) Zaczynajac od nowej architektury. Czyli Np trzymac wszytkie zapytania w jednym miejscu np w klasach repository. Business logic moge trymac np w klasach services itd itd. Nie chce za bardzo uzywac hexagon architecture czy DDD.

Czy moze ktos mial podobnie? Jaka architekture czy wzorce polecacie? Oczywiscie zostaje przy MVC, ale chce usunac logice biznesowac z widokow oraz kontrolerow i ja zcentralizowac w services. Zrobic light controllers. Jakis SOLID zaimplementowac etc. I na tych nowych klasach napisac test jednostowe i troche integracyjnych.

Zacny pomysł, popieram całym sercem.

Kilka słów ode mnie:

  • Pamiętaj że nie tylko query i sql mają być w repozytoriach, tylko każda logika jaka ma związek z persystencją. Jeśli wykonujesz jakieś mapowanie, jakiegoś ifa, jakąś pętle, która jest związana z bazą, to ona też się powinna znaleźc w repozytorium, nie tylko samo query.
  • Pomysł z lekkimi kontrolerami bardzo fajny, bo jakby nie patrzeć one są tylko po to żeby przetłumaczyć HTTP na żądania biznesowe.

Ale nie zgadzam się z Twoim podejściem do testów - nie powinieneś testować tylko tych klas service, tylko wszystko IMO.

0

Zgadzam sie, ze wszytko powinno byc przetestowane wlacznie z controllers, models, services i repositories. Tylko czy znajde na to czas :) W sumie dlaczego logika gdzie jest if, else czy foreach ma byc w repository? Jezeli jest to logika biznesowac to powinna byc w service. Czy chodzi Ci o SLQ transaction, o bloki z SQL queries?

0
poniatowski napisał(a):

Zgadzam sie, ze wszytko powinno byc przetestowane wlacznie z controllers, models, services i repositories. Tylko czy znajde na to czas :) W sumie dlaczego logika gdzie jest if, else czy foreach ma byc w repository? Jezeli jest to logika biznesowac to powinna byc w service. Czy chodzi Ci o SLQ transaction, o bloki z SQL queries?

Nie patrz na formę.

Logika związana z persystencją powinna być w repozytorium. Jeśli mapujesz coś, tylko po to żeby wsadzić to do bazy to jasno widać że to jest logika persystencji, nie biznesowa.

0

To co bedzie logika biznesowa w takim razie?

1
poniatowski napisał(a):

To co bedzie logika biznesowa w takim razie?

Nie wiem co ta aplikacja robi, ale takie typowe przykłady logiki biznesowej to

  • sposób wyliczenia podatku
  • obsługa reklamacji
  • wyliczenie polisy OC
  • określenie ryzyka kredytowego
  • obieg dokumentów
  • fraud detection
    itd.

Jak nie masz tego typu use case'ów w aplikacji to nie masz tym samym logiki biznesowej a twoja aplikacja to zwykła przeglądarka do bazy danych gdzie sprawdzi się prosta architektura kontroler -> serwajs -> baza (orm/repo/dao)

1
poniatowski napisał(a):

To co bedzie logika biznesowa w takim razie?

Logiką biznesową powinno być to co aplikacja robi. To co użytkownik korzystający z niej może zrobić, problem który aplikacja rozwiązuje. Swoisty sens czemu ta aplikacja istnieje powinien być w logice biznesowej.

0

Perystencja - repozytorium:
załóżmy że w pętli musisz zapisać X wierszy nadając im cos unikalnego. Niech będzie to rabat na wszystkie produkty z koszyka. Rabat jest unikalny per produkt.
/Oczywiście to najlepiej powinien być najlepiej jeden insert z perspektywy optymalizacji - to przykład/

Logika biznesowa - serwis:
Liczenie tego rabatu per produkt. Niech będzie tu np. zależność od ilości zakupionych produktów, kuponów, zniżek per kupujący.

1

Problem jest taki, ze w obecnej architekturze mamy wszystko w kontrolerach i modelach. I chce te takie if else czy foreach zabrac z kontrolerow czy modeli i dac osobno w klasach. Czyli SQL osobno, jakies wieksze operacje tez osobno. Moze to nie bedzie taka zawsze biznesowa logika, ale jak mam sporo operacji if else, switch foreach etc to nie chce tego pchac w model. Wole trzymac sie SOLID. Porozbijac to na mniejsze klasy. Napisac testy do tych klas. Nie chce tworzyc modelow orm ktore beda mialy tysiace linji kodu. Jezeli mam User ORM to nie chce wzsytkiego trzymac w tej klasie User.

1
poniatowski napisał(a):

Problem jest taki, ze w obecnej architekturze mamy wszystko w kontrolerach i modelach. I chce te takie if else czy foreach zabrac z kontrolerow czy modeli i dac osobno w klasach. Czyli SQL osobno, jakies wieksze operacje tez osobno. Moze to nie bedzie taka zawsze biznesowa logika, ale jak mam sporo operacji if else, switch foreach etc to nie chce tego pchac w model. Wole trzymac sie SOLID. Porozbijac to na mniejsze klasy. Napisac testy do tych klas. Nie chce tworzyc modelow orm ktore beda mialy tysiace linji kodu. Jezeli mam User ORM to nie chce wzsytkiego trzymac w tej klasie User.

No to jest bardzo dobry pomysł.

Rozumiem że teraz ma spaghetti, czyli pomieszane wszystko ze wszystkim?

Tylko widzisz, musisz być ostrożny. To jest bardzo dobry pomysł, że chcesz je poprzenosić do odpowiednich miejsc, ale niestety nie możesz tego zrobić automatycznie :/ Musisz przeczytać kod, i go zrozumieć całkowicie, i wskazać miejsca: ten kod to jest logika, ten kod to jest persystencja, ten kod to jest HTTP, ten kod to jest cache, ten kod to jest sesja. I to niestety nie jest takie proste jak "ify to logika, query to persystencja". Czasem jeden i ten sam kod w różnych kontekstach może należeć do dwóch domen.

Jak masz ułożony kod w odpowiednie klasy, to on właśnie dlatego jest dobrej jakości, bo ktoś włożył wysiłek w to żeby przeczytać kod i go poprzenosić. Możesz to przyrównać do sprzatania. Jak sprzątasz mieszkanie, to to nie jest takie proste że po prostu wszystko co jest na stole wsadzasz do szafki (Albo do kosza). Tylko bierzesz po kolei każdą rzecz ze stołu, i zastanawiasz się "gdzie ona należy", i przenosisz ją tam. Czasem nawet jeden i ten sam przedmiot możesz włożyć w różne miejsca (typu mała szklanka tu, duża szklanka tam). Podobnie jest z kodem. Każda linijka, każdy if, każda pętla, każda funkcja została dodana do kodu po coś, albo dlatego że tego klient wymaga, albo pod persystencję, albo pod HTTP. Czytając kod musisz zrozumieć po co konkretny kawałek jest, i go wynieść. Często jest tak że jakiś for ma pomieszane odpowiedzialności (czyli np załatwia jednocześnie bazę i logikę biznesową), takie rzeczy bardzo ciężko jest naprawić (np. klient wymaga unikanlnego indetyfikatora dla akcjonariusza, ale baza wymaga unikalnego id, i ktoś mógł napisać pętle która robi te dwie rzeczy na raz - takie coś jest bardzo ciężko posprzątac).

Możesz wkleić kilkanaście linijek takiego kontrolera? To Ci pokaże co mam na myśli.

0

To jest kawalek takiego jednego z kontrolera. Troche go przeriobilem, ale jak ponizej. Czy jakos nazywas sie ta architektura, ktora opisujesz? Czy to jest pod DDD albo hexagonalna? Pytam zeby mogl poczytac i nauczyl sie rozrozniac warstwy. Dzieki


class MyController extends Controller
{

    public function actionUploadFile($userId):
    {

        $user = $this->findModel($userId);

        $file = UploadedFile::getInstance('file');
        $fileName = sanitizeFile($file->name);

        $error = null;
        if ($file->size > 300000) {
            $error = 'Your file size must be no more than 300kb';
        } else if (in_array($file->extension, User::$validExtensions)) {
            $s3 = Yii::$app->aws->get('s3');
			
            if (!$s3->doesObjectExist(param('aws-bucket'), 'direction/' . $user->id . '/' . $fileName)) {
                $s3->putObject([
                    'Bucket' => param('aws-bucket'),
                    'Key' => $user->id . '/' . $fileName,
                    'SourceFile' => $file->tempName,
                    'ContentType' => $file->type,
                    'CacheControl' => 'max-age=50000000',
                ]);
                User::flushCache($user->id);
            } else {
                $error = 'The file name exists.';
            }
        } else {
            $error = 'Your file must have PGE extension';
        }

        $files = [
            [
                'url' => 'url',                
                'size' => $file->size,
                'name' => $fileName,
                'error' => ($error ?: null),
            ],
        ];

        Yii::$app->response->format = 'json';
        return ['files' => $files];

    }

    protected function findModel($id): User
    {
        if (($model = User::findOne(['id' => $id, 'user_id' => Yii::$app->user->identity->userId])) !== null) {
            return $model;
        } else {
            throw new NotFoundHttpException('No user found.');
        }
    }


}
1

No to moim zdaniem to jest tak:

screenshot-20231108112951.png

Faktycznie tu jest pomieszane wszystko.

0
poniatowski napisał(a):

Chce troche poprawic ten projekt :) Zaczynajac od nowej architektury. Czyli Np trzymac wszytkie zapytania w jednym miejscu np w klasach repository.

sqle w klasach? a co w javie piszesz?

1
Miang napisał(a):
poniatowski napisał(a):

Chce troche poprawic ten projekt :) Zaczynajac od nowej architektury. Czyli Np trzymac wszytkie zapytania w jednym miejscu np w klasach repository.

sqle w klasach? a co w javie piszesz?

A co w tym zlego? Musze jakos scentralizowac wszystkie zapytania. Na chwile obecna te same zapytania SQL sa powtarzane i uzywane wszedzie tj kontroler, widok, model etc etc.

0
  • ActionController
    • Responder
      • ActionService
        • Action
          • Repository
        • ActionService
        • Service

ActionController wyglada tak:

class ActionController
{
    public function __construct(
        private readonly JsonResponderInterface $responder
    ) {
    }

    public function __invoke(
        ResponseInterface $response,
        ActionServiceInterface $actionService,
        ServerRequestInterface $request,
    ): ResponseInterface {
        return ($this->responder)(
            $response,
            $actionService,
            $request
        );
    }
}

Napisany raz, obsluguje wszystko, podobnie z responderem.

1 użytkowników online, w tym zalogowanych: 0, gości: 1