LydiaCMS consists of 13 production Composer packages, including the core, and three developer tools. Apart from the core, each package adds one capability and is installed separately.
Packages
- core — application bootstrap, DI, routing, permissions, translations and the base of the administration
- menu, blog, forms, attributes, currencies, location, socials, units — content modules, each with its own entities, routes and migrations
- integration, data, api, backup — import from web services, a configurable data model with version history, the REST/MCP interface, backups and restore
- phpstan, rector, ecs — developer tools, described in the section on quality
How a package plugs in
- It registers through a single Nette (DI) extension: entities, routes, services and migrations belong to their own package.
- Every package has a version and idempotent migrations. Updating the database is one command,
lydiacms:upgrade.
Generating the administration from metadata (metadata-driven CRUD scaffolding)
Grids and forms are generated from the metadata of Doctrine entities (over 100 entities). A custom template is written only where the data is not flat: the menu tree, the form builder or variants of social networks. A detail such as a help text or a placeholder is handled by an attribute on the field.
Multilingual data model and SEO
Every record exists in language variants (currently CS, EN, DE, FR, ES and SK) and each variant is a separate record in the database. Adding another language therefore needs no code change. The SEO data (title, description, image and slug) is set separately for every record and language.
Page addresses are stored in the database including the whole path through the ancestors. This page, for example, lives at /projects/lydiacms/architecture. A shortened address without the ancestors is permanently redirected (301) to the full path. When I change the slug of a page, the old address is redirected to the new one.
Permissions are inherited down the tree of contexts
Contexts form a tree (system → frontend, admin, API → package → submodule → record) and each of them is a source of ACL. A default rule is set once for the whole namespace (the administration and the API are closed, the frontend is open) and everything below inherits it. A new submodule or record therefore needs no rules of its own. A rule on a package applies to all its submodules, and a rule on a submodule, whether allowing or denying, overrides it.
- Access to the administration is derived from the rules. Whoever may display anything in the administration gets into it. There is no other switch.
- A permission ceiling. Nobody can allow or grant more than they have themselves, so nobody can raise their own rights.
- A preset for content work fills in the rules for pages, blog, menu and media with one click and never grants deleting, export, settings or management of users and roles. The result is ordinary rules that can be edited further in the matrix.
A permission (display, create, edit, delete, change state, export) has an admin, frontend or API scope and can be limited to contexts by a pattern with wildcards, for example api.core.**. The same ACL applies to the administration and to the interface for AI.
Media and cron jobs in the administration
For every file, the media library shows where it is used, so it is clear what deleting it would break. Cron jobs keep a record of every run with its result and the time of the next run.