docs(articles): English translation of From Magento to Symfony
en/articles/magento-to-symfony/quality/traps.md
new file +81
−0
Pitfalls
Where Magento experience lets you down most often. Red blocks are pitfalls that corrupt data, yellow ones cost you time.
flush() writes everything
Not just "this" entity, but every entity changed in the UnitOfWork. flush($entity) was removed in ORM 3. Keep flush at the entry point and don't modify entities you don't intend to save.
State in service properties
Services are shared, and a Messenger worker lives for hours and handles thousands of messages. A cache, the current user or an order stored in a service property leaks between messages. Implement ResetInterface or don't keep state.
EntityManager is closed
After an exception during flush, the EntityManager is closed and subsequent operations fail. Don't catch the exception to carry on with the same EM. In Messenger workers the Doctrine middleware takes care of this; in your own loops use ManagerRegistry::resetManager().
N+1 on lazy associations
The habit of "load a collection, then call getX() in a loop" generates a query per iteration here. Watch the query counter in the profiler and use a fetch join.
Changes inside an object go unnoticed
Doctrine compares values, not the contents of objects. $this->date->modify('+1 day') on a mutable DateTime won't be saved. Use DateTimeImmutable and assign a new value; the same goes for arrays of objects in JSON columns.
The inverse side isn't persisted
In a bidirectional association, only the owning side is written to the database. Add helpers that update both sides.
cache:clear doesn't clear data
The system cache and cache pools are different things. Stale data in Redis stays after cache:clear; use cache:pool:clear or tag-based invalidation for it.
$container->get() doesn't work
Services are private, so the ObjectManager approach is impossible. If you need to pick a service at runtime, use #[AutowireLocator].
An unused service disappears
The container removes private services that nobody injects. In tests, getContainer()->get() on such a service throws an error; this is not a config bug.
Listener order matters
The router on kernel.request has priority 32, the firewall has 8. A listener with priority 20 runs before authentication and doesn't see the user. Check with debug:event-dispatcher.
Old code in workers
A worker keeps the code in memory. After deployment run messenger:stop-workers, otherwise messages are handled by the previous version.
Secrets in .env
.env is committed, so it holds only default values for dev. Real keys go into the server's environment variables or into secrets:set.
Porting Magento layers one to one
An Api/Data interface, a Repository wrapper around the repository, a Management service for every entity: this is ceremony with no benefit in an application you own. Add a layer when there is a real reason for it.