src/ directory and the migration paths are fixed. That’s less limiting than it sounds: most plugins need settings and a little state, and both have a home.
Choose a storage strategy
Options
Options are key-value rows holding JSON. They’re loaded once per request and exposed to the container, so reading one is free after boot.Reading
option:
Writing
option.crm.api_key untouched. Lists are replaced wholesale, which is what you want for an array of configured items.
Options are global to the installation, not per workspace or per user. For per-workspace state, key the data inside the option by workspace ID, or store it on a core entity.
Core entities
Your plugin can read and write the core’s own data through its commands and repositories, which keeps validation and events intact:When you really need tables
If your plugin genuinely needs its own tables, own them end to end:- Create the schema from an
install()hook, using the DBAL connection from the entity manager, and drop it inuninstall(). - Query with DBAL rather than the ORM, since Doctrine won’t map your entities.
- Namespace table names with your vendor, such as
acme_notes, so you never collide with a future core table.
Caching
For values that are expensive to compute or fetch, use the PSR-6 cache pool instead of a new option:Cleaning up
Uninstalling a plugin removes its files, but not its data. Delete what you own in theuninstall() hook: