Project structure#
The root#
| Folder | Contents |
|---|---|
app/ |
all the application's PHP code |
config/ |
the configuration (NEON) |
www/ |
the public root, themes, assets, uploaded files |
docs/ |
documentation — 🔴 served statically, outside the application |
manual/ |
the sources of this manual (MkDocs) |
tests/ |
the tests |
skills/ |
the binding procedures |
improvements/ |
working notes and plans |
_legacy/ |
the previous generation of the system |
temp/, log/, var/ |
runtime data |
docs/ is a public path outside the application's authorisation
Whatever you generate there is downloadable by anyone — and the application never even
learns about the request, so there is nowhere to log it. Outputs containing data belong
in var/. In detail: CORS, rate limiting and audit.
app/Core/ — the shared foundation#
| Folder | Contents |
|---|---|
Base/ |
the layers' base classes, split by section plus Shared/ |
Bridge/ |
ApiBridge — the bridge between Front/Admin and Api |
Traits/ |
shared implementations, split into Shared / Api / Admin / Front / Cron |
Utils/Hydrators/ |
both hydrators |
Utils/Filters/ |
the composable filters |
Security/ |
JWT, RBAC, CORS, rate limiting |
Forms/ |
the forms core; Form/Parameters/ the dynamic parameter system |
Latte/ |
template extensions and macros |
Routers/ |
routing, route factories and translators |
Cache/ |
the query cache and the route cache |
Attributes/ |
attributes for entities and actions |
Integrations/ |
Elasticsearch and others |
Theme/ |
selecting the active theme |
app/UI/ — section × module#
The most important division in the whole project. The section determines the entry point, the module determines the domain.
app/UI/
Front/ ← the public site
Api/ ← the only section with real database access
Admin/ ← the administration
Cron/ ← scheduled tasks
Scripts/ ← one-off and maintenance scripts
Script/ ← an older namespace for the same (System only)
Error/ ← error pages (Error4xx, Error5xx)
Base/ Bazaar/ Blog/ Comcat/ Discussion/ Eshop/ Invoicer/ Store/ System/
app/UI/Admin/Eshop/ is the e-shop's administration, app/UI/Api/Eshop/ its API. The
same tables, a different entry point, different rules.
Script/ and Scripts/ are two different sections
The namespace determines the translation domain, so a presenter in
App\UI\Scripts\* looks its texts up in the scripts domain. Creating a script in the
other one means the texts are not found — and that surfaces as empty labels, not as an
error.
A module's structure#
app/UI/<Section>/<Module>/
Presenters/
XxxPresenter.php
Traits/
Inits/ initialisation, injection, the topic's signals
Lists/ listings
Details/ details
Forms/ forms
Pickers/ Picker configurations (the administration)
Components/
XxxComponent/
Xxx.php XxxFactory.php Traits/ Templates/Default/
Templates/
Models/
Entities/ Managers/ Services/ Mappers/ Repositories/
Generators/ Helpers/ Search/ (as needed)
A presenter should be thin, the logic belongs in traits
A trait for the listing, a trait for the form, a trait for the detail. The presenter
file is then just a list of use statements, properties and a constructor — see
Code conventions.
Where an entity exists three times over#
The same entity exists in several shapes at once, and that is deliberate, not duplication:
| Where | What it is |
|---|---|
app/Core/Base/Shared/<Module>/Models/Entities/ |
the abstract ancestor with the columns and accessors |
app/UI/Api/<Module>/Models/Entities/ |
the Doctrine entity over the table |
app/UI/Admin/<Module>/Models/Entities/ |
what the administration needs |
app/UI/Front/<Module>/Models/Entities/ |
what the public site needs |
In detail: Hydrators and the Five-layer model.
Legacy#
_legacy/ in the root is the previous generation of the system. Inside app/ there are
three further Legacy/ subfolders — leftovers not yet converted.
Do not touch legacy code and do not copy patterns from it
It is marked obsolete and will be removed. When adding a feature, write it the new way —
the relevant skill in skills/ describes how. What makes it dangerous is that legacy
code looks like a valid model, because it sits in the same project.
Where a new class belongs#
| I am writing | It belongs in |
|---|---|
| domain logic | app/UI/Api/<Module>/Models/Managers/ |
| an SQL query | app/UI/Api/<Module>/Models/Repositories/ |
| process orchestration (checkout, a state transition) | a manager-tier class next to the entity manager, not in Services/ |
| an artefact generator (PDF, export) | app/UI/Api/<Module>/Models/Generators/ |
| a pure computation with no I/O | app/UI/Api/<Module>/Models/Helpers/ |
| an administration screen | app/UI/Admin/<Module>/Presenters/ |
| a scheduled task | app/UI/Cron/<Module>/Presenters/ |
| something two sections need | app/Core/ |
| translation keys | app/Locale/<module>/<locale>/<domain>.<LOCALE>.neon |
Follow-up chapters: Five-layer model · Code conventions · Modules overview · Translations