Verwaiste Symfony-Bundles ersetzen: SensioFrameworkExtra, Guard, Swiftmailer, FOSRest, FOSUser
Was jedes verwaiste Bundle getan hat, was es in Symfony 6.4 und 7.4 ersetzt, und welche Codeänderung dahintersteckt, mit Vorher-Nachher-Beispielen.
Veröffentlicht 2026-09-12. Geschrieben für Teams, die das Upgrade selbst machen; wer es lieber machen lässt, findet hier Upgrades zum Festpreis. Auch verfügbar auf English.
Warum verwaiste Bundles über das Upgrade entscheiden
Ein verwaistes Paket hat kein Release, das die nächste Symfony-Major-Version erlaubt. Wie klein es ist, spielt keine Rolle: Composer verweigert das Update, bis es weg ist. In einer 5.4-Codebasis ist die Liste der verwaisten Pakete meist dieselben fünf Namen, und jeder davon entspricht einer Funktion, die Symfony in den Core übernommen hat. Dieser Leitfaden zeigt pro Bundle, was es tat, was es ersetzt, und den Code vorher/nachher. Die Kompatibilitätsseiten listen den Verwaist-Status der 500 meistgenutzten Bundles.
SensioFrameworkExtraBundle
Verwaist seit Symfony 6.2 seine Funktionen im Core ausliefert. Lässt sich nicht installieren auf Symfony 7. Seine Annotations und ihre Core-Entsprechungen:
| Annotation | Core-Ersatz | Seit |
|---|---|---|
@Route | #[Route] (symfony/routing) | 5.2 |
@ParamConverter | #[MapEntity] (doctrine-bundle) und #[MapDateTime] | 6.2 |
@IsGranted, @Security | #[IsGranted] mit Attribut oder new Expression() | 6.2 |
@Cache | #[Cache] (http-kernel) | 6.2 |
@Template | Explizites $this->render(); bewusst kein Ersatz | – |
// Vorher
/**
* @Route("/customers/{id}/orders/{orderId}", name="customer_order")
* @ParamConverter("order", options={"id": "orderId"})
* @Security("is_granted('VIEW', order) and order.customer == customer")
* @Cache(maxage=300, public=true)
* @Template("order/show.html.twig")
*/
public function show(Customer $customer, Order $order): array
{
return ['customer' => $customer, 'order' => $order];
}
// Nachher
#[Route('/customers/{id}/orders/{orderId}', name: 'customer_order')]
#[IsGranted(new Expression("is_granted('VIEW', subject['order']) and subject['order'].customer == subject['customer']"), subject: ['order', 'customer'])]
#[Cache(maxage: 300, public: true)]
public function show(#[MapEntity] Customer $customer, #[MapEntity(id: 'orderId')] Order $order): Response
{
return $this->render('order/show.html.twig', ['customer' => $customer, 'order' => $order]);
}
@ParamConverter mit einer eigenen Converter-Klasse (die ParamConverterInterface implementiert) wird zu einem ValueResolverInterface-Service mit dem Tag controller.argument_value_resolver; das Interface hat eine Methode, resolve(Request $request, ArgumentMetadata $argument): iterable. Das 6.2-Release des Bundles leitet seine Annotations an die Core-Attributes weiter, sodass Sie es auf 6.x installieren können, während Sie migrieren, und es am Ende entfernen. Rectors SymfonySetList::ANNOTATIONS_TO_ATTRIBUTES konvertiert @Route, @IsGranted und @Cache; @ParamConverter und @Template sind Handarbeit.
symfony/security-guard (Guard-Authenticator)
Entfernt in Symfony 6.0. Ersatz: das Authenticator-System in symfony/security-http, als Opt-in auf 5.4 verfügbar (enable_authenticator_manager: true). Das Denkmodell wechselt von "sieben Methoden, die Credentials, User und Booleans zurückgeben" zu "ein authenticate(), das einen Passport aus Badges zurückgibt".
// Vorher: Formular-Login-Guard (typische make:auth-Ausgabe aus 4.x/5.x)
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator implements PasswordAuthenticatedInterface
{
public function supports(Request $request) { return 'app_login' === $request->attributes->get('_route') && $request->isMethod('POST'); }
public function getCredentials(Request $request) { /* E-Mail, Passwort, CSRF lesen */ }
public function getUser($credentials, UserProviderInterface $userProvider) { /* User finden */ }
public function checkCredentials($credentials, UserInterface $user) { return $this->hasher->isPasswordValid($user, $credentials['password']); }
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey) { /* Redirect */ }
protected function getLoginUrl() { return $this->urlGenerator->generate('app_login'); }
}
// Nachher
class LoginFormAuthenticator extends AbstractLoginFormAuthenticator
{
use TargetPathTrait;
public const LOGIN_ROUTE = 'app_login';
public function __construct(private UrlGeneratorInterface $urlGenerator) {}
public function authenticate(Request $request): Passport
{
$email = $request->getPayload()->getString('email');
$request->getSession()->set(SecurityRequestAttributes::LAST_USERNAME, $email);
return new Passport(
new UserBadge($email),
new PasswordCredentials($request->getPayload()->getString('password')),
[new CsrfTokenBadge('authenticate', $request->getPayload()->getString('_csrf_token')), new RememberMeBadge()],
);
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
return new RedirectResponse($this->getTargetPath($request->getSession(), $firewallName) ?? $this->urlGenerator->generate('app_dashboard'));
}
protected function getLoginUrl(Request $request): string
{
return $this->urlGenerator->generate(self::LOGIN_ROUTE);
}
}
# security.yaml
security:
password_hashers: # war 'encoders'
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: auto
firewalls:
main:
lazy: true # ersetzt 'anonymous: true'
provider: app_user_provider
custom_authenticator: App\Security\LoginFormAuthenticator
logout: { path: app_logout }
remember_me: { secret: '%kernel.secret%' }
Einfache Formular-Logins und HTTP Basic brauchen keine Klasse mehr: form_login:, json_login:, http_basic:, access_token: (6.2+) und login_link: sind eingebaut. Die meisten eigenen Guards waren API-Key- oder JWT-Prüfungen; die werden zu AbstractAuthenticator mit einem SelfValidatingPassport, oder zu access_token: mit einem AccessTokenHandlerInterface, das ein UserBadge zurückgibt.
Swiftmailer und symfony/swiftmailer-bundle
Verwaist; das Bundle hat kein Symfony-6-Release. Ersatz: symfony/mailer (stabil seit 4.4) mit symfony/mime. Transports werden per DSN konfiguriert, HTML-Mails sind Twig-Templates, asynchroner Versand ist Messenger.
// Vorher
$message = (new \Swift_Message('Your invoice'))
->setFrom(['billing@example.com' => 'Billing'])
->setTo($customer->getEmail())
->setBody($this->twig->render('emails/invoice.html.twig', ['invoice' => $invoice]), 'text/html')
->attach(\Swift_Attachment::fromPath($pdfPath));
$this->swiftMailer->send($message);
// Nachher
$email = (new TemplatedEmail())
->from(new Address('billing@example.com', 'Billing'))
->to($customer->getEmail())
->subject('Your invoice')
->htmlTemplate('emails/invoice.html.twig')
->context(['invoice' => $invoice])
->addPart(new DataPart(new File($pdfPath), 'invoice.pdf', 'application/pdf'));
$this->mailer->send($email); // Symfony\Component\Mailer\MailerInterface
# .env
MAILER_DSN=smtp://user:pass@smtp.example.com:587
# config/packages/messenger.yaml, für das alte Spool-Verhalten
framework:
messenger:
routing:
Symfony\Component\Mailer\Messenger\SendEmailMessage: async
Für Swiftmailer geschriebene Twig-Templates funktionieren unverändert. Inline-CSS über twig/cssinliner-extra und Markdown über twig/markdown-extra ersetzen die üblichen Swiftmailer-Plugins. Swift_Events-Plugins werden zu MessageEvent-Listenern.
FOSRestBundle
Status: minimal gepflegt; die Maintainer empfehlen seit 2021 API Platform, und 3.x lässt sich auf Symfony 7 installieren, bietet aber nichts, was der Core nicht kann. Zwei Auswege, je nachdem, wofür Sie es genutzt haben.
Sie haben es für View-Handling und Format-Negotiation in klassischen Controllern genutzt. Das kann der Core jetzt:
// Vorher
/** @Rest\Get("/orders/{id}") @Rest\View(serializerGroups={"order:read"}) */
public function getOrder(Order $order) { return $order; }
/** @Rest\Post("/orders") @ParamConverter("input", converter="fos_rest.request_body") */
public function postOrder(CreateOrder $input, ConstraintViolationListInterface $violations) { /* $violations prüfen */ }
// Nachher
#[Route('/orders/{id}', methods: ['GET'])]
public function getOrder(#[MapEntity] Order $order): JsonResponse
{
return $this->json($order, context: ['groups' => ['order:read']]);
}
#[Route('/orders', methods: ['POST'])]
public function postOrder(#[MapRequestPayload] CreateOrder $input): JsonResponse
{
// bereits validiert: eine ConstraintViolation führt zu 422, bevor die Action läuft (Symfony 6.3+)
}
Sie haben es als API-Framework genutzt (Routing nach Resource, ParamFetcher, Body-Listener, Exception-Formatierung). Dann ist der Ersatz API Platform, ein Resource-Set nach dem anderen, mit stabilen URLs über explizite uriTemplate-Werte. FOSRest und API Platform können während der Migration in derselben Anwendung koexistieren; geben Sie jedem ein eigenes Routen-Präfix und eine eigene Firewall.
FOSUserBundle
Status: gepflegt, aber Legacy; es koppelt die User-Entity an einen Security-Stack, den Symfony zweimal ersetzt hat. Ersatz: mit MakerBundle generierter Code plus zwei kleine Bundles:
bin/console make:user # Entity + Provider
bin/console make:security:form-login # Authenticator, Login-Controller, Template (6.2+)
bin/console make:registration-form # mit symfonycasts/verify-email-bundle
bin/console make:reset-password # mit symfonycasts/reset-password-bundle
Behalten Sie die fos_user-Tabelle und mappen Sie die generierte User-Entity darauf (gleiche Spalten: username, email, password, roles, enabled). Von FOSUser mit bcrypt oder Argon2 gehashte Passwörter funktionieren mit password_hashers: { ...: auto } weiter. Gruppen werden zu einfachen Rollen oder einer eigenen Group-Entity; die Gruppenfunktion des Bundles ist das Einzige ohne Drop-in-Ersatz.
doctrine/annotations und der Rest
- doctrine/annotations: verwaist. Nichts in Symfony 7, Doctrine ORM 3, API Platform 3+ oder PHPUnit 12 liest Annotations. Mit Rector konvertieren und das Paket entfernen; siehe den ORM-Leitfaden.
- doctrine/cache, doctrine/doctrine-cache-bundle: ersetzt durch
symfony/cache; DoctrineBundle konfiguriert ORM-Caches mit Cache-Pools. - twig/extensions:
twig/extra-bundlemittwig/intl-extra,twig/string-extra.localizeddatewird zuformat_datetime. - symfony/templating: Twig direkt.
- sensiolabs/security-checker:
composer audit. - symfony/security-acl: nicht verwaist, aber praktisch ungepflegt; Voter auf den eigenen Entities ersetzen es in fast jedem Fall.
Die richtige Reihenfolge
All das ist auf Symfony 5.4 mit PHP 8.1 möglich, und dort sollte es passieren: ein Bundle pro Pull Request, jeder für sich deploybar, sodass der spätere Sprung auf 6.4 nur noch Konfiguration und composer.json berührt. Der Leitfaden 5.4 auf 6.4 ordnet diese Schritte in den Rest des Upgrades ein.
Welche verwaisten Pakete stehen in Ihrer composer.lock?
Der Scan markiert jedes verwaiste Paket mit seinem empfohlenen Ersatz und zeigt, welche davon den Symfony-Zweig blockieren, den Sie anstreben.
Weitere Leitfäden
- Upgrade von Symfony 5.4 auf 6.4: der vollständige Leitfaden
- Upgrade von Symfony 6.4 auf 7.4: Deprecations, Return Types und der Sprung
- API Platform 2.6 auf 3: Resources, Data Provider und Persister migrieren
- API Platform 3 auf 4: der State-basierte Ablauf, RFC-7807-Fehler und Hydra-Änderungen
- Upgrade von Doctrine ORM 2 auf 3 (und DBAL 3 auf 4) in einer Symfony-Anwendung