Symfonyból érkezőknek
Ha Symfonyval dolgoztál, a gpsystem gondolkodásmódja ismerős lesz: a Messenger, az EventDispatcher és a Voterek mögötti fogalmak egy az egyben átjönnek. Ez az oldal megmutatja, mi minek felel meg, és őszintén azt is, mi nem transzferálódik.
Ami átjön: az üzenet-alapú aszinkron feldolgozás, az esemény/listener-gondolkodás és a kérésenkénti jogosultság-ellenőrzés fogalmi kerete. Ami nem: nincs service container és autowiring (a függőségek egy explicit Dependencies structban utaznak, amit a main.go-ban te töltesz ki), nincs bundle-rendszer, és nincs Doctrine ORM: a repository-kban SQL-t vagy bun query buildert írsz.
Megfeleltetési tábla
| Symfony | gpsystem | Hol |
|---|---|---|
Scheduler komponens (#[AsPeriodicTask]) | scheduler.Schedule fluent API (sched.DailyAt(...), sched.Job(...)) | Ütemezés |
Messenger (#[AsMessageHandler]) | events.Event + events.Listen (minden listener önálló, újrapróbált task) | Események |
Messenger transport, worker (messenger:consume) | queue (asynq + Valkey) + cmd/worker bináris | Queue, Worker |
Security Voterek (voteOnAttribute) | policy.Registry + generált enforcer middleware, fail-closed | Policy-k |
Role hierarchy, ROLE_* | rbac.Identity, RequireRole / RequirePermission | RBAC |
| Security firewall, authenticatorok | auth (JWT kibocsátás és middleware) | Autentikáció |
| MonologBundle (dev channel) | logx/monolog (Monolog-formátumú konzol-handler) | Logolás |
Doctrine Migrations (doctrine:migrations:migrate) | go run ./cmd/migrate (beágyazott, számozott SQL) | Migrációk |
EntityManager::wrapInTransaction() | Transactor.WithinTransaction(ctx, fn) (a tranzakció a contextben utazik) | Tranzakciók |
Validator attribútumok (#[Assert\NotBlank]) | validator struct-tagek + generált request-típusok | Validáció |
Exception listenerek, ExceptionEvent | errs kódok + public üzenet → RFC 9457 problem+json / Sentry | Hibamodell |
config/packages/*.yaml + .env | envconf.MustLoad[T] (típusos struct env-ből, .env-támogatással) | Konfiguráció |
| Flysystem bundle (oneup) | a storage modul Driver/Disk/Manager modellje + S3-driver | Tárolás |
| Mailer komponens | mail.Message builder + MJML template-ek; a memória-mailer a teszt-dupla | |
| Notifier komponens | notify (multi-csatornás értesítések) | Értesítések |
| Mercure | realtime (first-party publish/subscribe) | Realtime |
MakerBundle (make:*) | go tool gpsystem new/add ... | CLI |
| Pagerfanta / KnpPaginatorBundle | paginate.Page[T] / paginate.CursorPage[T] | Lapozás |
| Doctrine ORM | pgx (SQL) vagy bun (query builder), nincs ActiveRecord/Doctrine-szimuláció | Adatbázis |
Route-annotációk (#[Route]), auto-discovery | Register<Surface>(router, deps) (explicit hívás a main.go-ban) | Architektúra |
| Külön controller-készlet közönségenként (publikus API vs admin firewall mögött) | surface (felület-köteg): surfaces/<surface>/, saját kontrakttal és middleware-rel | add surface |
| Entity-metódusok + közös service-logika | a modul core/ csomagja (egyszer leírt szabályok, minden surface hívja) | Projektstruktúra |
| Doctrine repository-k | a modul-szintű repository/: modulonként egy, minden surface közösen használja | Projektstruktúra |
Két példa egymás mellett
Messenger handler: egy asztali üzenet feldolgozása:
#[AsMessageHandler]
final class OrderPlacedHandler
{
public function __invoke(OrderPlaced $message): void
{
$this->mailer->send(
(new TemplatedEmail())
->to($message->email)
->htmlTemplate('emails/order_confirmation.html.twig')
->context(['orderId' => $message->orderId]),
);
}
}
func sendOrderConfirmation(deps shop.Dependencies) func(context.Context, shopevents.OrderPlaced) error {
return func(ctx context.Context, ev shopevents.OrderPlaced) error {
msg := mail.NewMessage().
WithTo(ev.Email).
WithSubject("Rendelésed visszaigazolása").
WithBody(mjml.Template(templates, "templates/order_confirmation.mjml.tmpl",
map[string]any{"OrderID": ev.OrderID}))
return deps.Mailer.Send(ctx, msg)
}
}
Jogosultság-ellenőrzés: kérésenkénti tulajdonos-ellenőrzés, amikor a szerep önmagában kevés:
class OrderVoter extends Voter
{
protected function voteOnAttribute(string $attribute, mixed $order, TokenInterface $token): bool
{
$user = $token->getUser();
return $order->getUserId() === $user->getId()
|| in_array('ROLE_ADMIN', $user->getRoles(), true);
}
}
func New() *kitpolicy.Registry {
reg := kitpolicy.NewRegistry()
reg.Register("orders.view", func(ctx context.Context, id *rbac.Identity, req any) error {
if id.HasRole("admin") {
return nil
}
r := req.(gen.GetOrderRequest)
if !ownsOrder(ctx, id, r.OrderId) {
return kitpolicy.Deny("You can only view your own orders.")
}
return nil
})
return reg
}
Egy lényegi különbség: egy Symfony Voter abstain-nel is válaszolhat (nem dönt, adja tovább a láncnak), és a denyAccessUnlessGranted() hívás a controllerben elfelejthető. Itt a policy a TypeSpec-kontraktus @policy dekorátorával kötődik a művelethez, az enforcer middleware pedig a generált táblából kényszeríti ki. A megjelölt, de nem regisztrált policy nem átenged, hanem hibázik (fail-closed); nincs "abstain" állapot.
Viselkedésbeli különbségek
- Események és a fan-out. A Symfony Messenger egy üzenethez egy handlert rendel (több handler regisztrálható, de tipikusan egy retry-konfiguráció vonatkozik az üzenetre). A gpsystem minden listenert önálló, saját retry-kerettel futó asynq taskként kezel: egy event N listener-taskra terül szét, egy hibázó listener újrapróbálkozása sosem futtatja újra a többit. Részletek: Események.
- Tranzakciós outbox. A Doctrine
EntityManager::wrapInTransaction()a commit-előtti rollbacket kezeli, de a commit-utáni crash-ablakot (a folyamat a DB-commit és az üzenet-dispatch közt hal le) nem zárja be. A gpsystem outboxa gyárilag ugyanabba az adatbázisba, ugyanabban a tranzakcióban írja az eventet, mint az üzleti változást. Részletek: Outbox. - Migrációk. A Doctrine Migrations-nek van séma-diff generátora (
doctrine:migrations:diff); a gpsystemben nincs, csak sima, kézzel írt SQL, tudatos trade-off a diff-mágia elfedő hatása ellen. Részletek: Migrációk.
Mi nincs, és miért
- Nincs service container és autowiring. A
Dependenciesstruct kézzel épül amain.go-ban. Cserébe a "honnan jön ez a függőség?" kérdésre mindig egycmd/<modul>/main.go-beli sor a válasz. - Nincs Doctrine ORM. A repository-réteg pgx-szel (SQL) vagy bunnal (query builder) dolgozik, a Go-ökoszisztémában az explicit lekérdezés a bevett út. A bun opcionális adapter, ha a query builder kényelmét szeretnéd.
- Nincs Twig. A kit API-backendekre fókuszál; a szerveroldali renderelés nem része.
Háttér
A kit szerzőjének motivációja pont az volt, hogy a Go-ökoszisztémában is legyen egy, a batteries-included PHP frameworkökhöz hasonlóan könnyen használható rendszer: ahol a projekt első napján üzleti logikát írsz, nem infrastruktúrát válogatsz. A Go-világ ereje a kis, egymástól független, jól tesztelt library-kben van: a gpsystem nem lecserélni akarja ezeket, hanem egyetlen, összecsiszolt és karbantartott kompozíciót adni belőlük. A library-fél egy microservice chassis, a generált projekt egy service template (Richardson), nem egy futásidejű réteg, ami az app fölött él. A cél a kényelmes indulás, nem egy új, zárt keretrendszer.