Overview
An academic project from the fifth semester of the BUT degree, built by a team of three at IUT Montpellier-Sète. The goal: build a profile directory in Symfony — an application where users sign up, publish a profile card and decide whether it is publicly visible — an ideal playground for learning authentication, roles and data consistency.
The application is a classic Symfony monolith: pages are rendered server-side with Twig, persistence goes through Doctrine and MySQL, and everything runs inside a reproducible Docker environment.
- Two roles:
ROLE_USERbrowses the directory and manages their own profile,ROLE_ADMINalso sees hidden profiles and can delete any account - Full profile lifecycle: registration, login, viewing, editing, deletion
- Around two months of development, from September to October 2024
My role
The User entity and data consistency
I took ownership of the domain model — a single entity, but one that carries most of the application’s rules.
Userentity implementingUserInterfaceandPasswordAuthenticatedUserInterface, with core information (login, email, profile code, visibility, dates) and optional complementary fields (phone number, country, address)- Uniqueness constraints in the database on login, email and profile code, backed by
#[UniqueEntity]attributes so the user gets a clear message instead of an SQL error - Validation assertions placed directly on the entity: bounded lengths for login and profile code, email format checking
#[ORM\PreUpdate]callback updating the last-modified date — with one subtlety: if the only changed field is the last-connection date, the modification date is left alone, otherwise merely logging in would look like a profile edit
Profile codes: generation and live uniqueness checking
Every user owns a unique profile code, either chosen by them or generated by the application. It is the most interesting part of the project, because it cuts through the entire stack — from the database to the JavaScript.
- Server-side generation in
UserManager: an alphanumeric string of random length between 4 and 20 characters, regenerated as long as the drawn code is already taken - Two dedicated JSON routes,
checkProfileCodeandgenerateProfileCode, exposed to the browser through FOSJsRoutingBundle rather than hard-coding URLs in the JavaScript - Live availability checking while typing, with debouncing so a request isn’t fired on every keystroke
- The current code is ignored when editing, otherwise users would be told their own code is already taken
JSON API
- Two read endpoints: fetch a profile by its login (
/api/profile/login/{login}) or by its profile code (/api/profile/profileCode/{profileCode}) - Explicit serialisation: exposed fields are listed one by one in the controller, guaranteeing that no sensitive data — starting with the hashed password — can leak by accident
- Structured JSON
404response when the profile does not exist
Profile editing and tooling
- A separate edit form from the registration form, with an optional password change: if no new password is entered, the existing hash is kept as is
- Phone number validation by regular expression on the server side, surfaced back into the form as a field-level error
create:userconsole command to provision an account — administrators included — from the terminal, essential for seeding the database without going through the UI- Custom error pages (403, 404, 500) for the production environment
Architecture and security
- Service layer with interfaces:
UserManager,FlashMessageHelperandUserFormHelperare injected through their interface rather than their implementation, keeping controllers thin and putting dependency inversion into practice - Form-based authentication configured in
security.yaml, with CSRF protection enabled, identification on theloginfield and POST-only logout - Expression-based authorisation: the
#[IsGranted]attributes on the edit and delete routes only allow the action for the account owner or an administrator - Password hashing delegated to Symfony’s
UserPasswordHasherwith theautoalgorithm - Authentication event subscriber updating the last-connection date on successful login and pushing flash messages for login, failure and logout
- Maintenance mode: a high-priority
kernel.requestlistener redirects all traffic to a dedicated page, with the flag driven by a console command
Twig interface
- Split, factorised templates: a shared
base.html.twig, domain-specific views (profiles, users, utility pages) and reusable fragments included on both sides — the profile-code widget is shared between registration and editing - Form fields factorised in
UserFormHelper, avoiding duplicated field definitions between registration and editing - Flash messages centralised in the layout, fed by a service that turns form validation errors into readable messages
- Conditional rendering based on login state and role: navigation, edit and delete buttons only appear when the user is allowed to use them
- AssetMapper with importmap, Stimulus and Turbo, with no front-end build step to install
Infrastructure
- Docker Compose with two services: a custom-built Apache/PHP web server and a MySQL database persisted on a volume
- Local HTTPS through a self-signed certificate, alongside plain HTTP
- Versioned Doctrine migrations to rebuild an identical schema on any machine
- Step-by-step installation documented in the README, from starting the containers to creating the database
Teamwork
- Setting up the project organisation: creating and running the Trello board, configuring the repository and its structure
- Explicit task split — entity, API, editing and profile codes on my side, CSS integration and home page for Téo, profile deletion and visibility for Victor
- Version control on the IUT GitLab, with the repository later mirrored to GitHub
- Regular check-ins and cross reviews, particularly valuable around permissions, where one member’s decision directly changes how everyone else’s code behaves
Technologies
- Back-end: Symfony 6.4, PHP 8.1+, Doctrine ORM 3, FOSJsRoutingBundle
- Front-end: Twig, AssetMapper and importmap, Stimulus, Turbo, JavaScript, CSS
- Database: MySQL, Doctrine migrations
- Infrastructure: Docker Compose, Apache
What I took away
- Constraints belong in the right place: between assertions on the entity and assertions on the form, the rule that actually protects the data is the one living on the entity — the single most useful piece of feedback from the project’s review
- A simple feature cuts through the whole stack: the profile code took me from a database uniqueness constraint to a domain service, an exposed route and the JavaScript, right down to debouncing to make typing feel pleasant
- Permissions are designed before they are coded: expressing them inline works, but grouping them into voters would have made the rules far more readable and testable — it is the first thing I would do differently

