Training catalog — technical guide
Engineering reference for course discovery, administration and consuming review workflows. The observed standalone catalog is legacy; nextgen has training APIs and a catalog modal inside reviews. See functional for daily usage and configuration for administrator ownership. Code evidence does not establish the deployed revision or successful runtime behavior.
Architecture and responsibilities
Verified in code: legacy controllers/templates use Doctrine; nextgen Infrastructure/Persistence/Entity/Training/Formation.php maps the same named formation table as legacy Formation.orm.yml. This establishes compatible persistence mappings, not proof that local repositories point to the same deployed database. The legacy new_formation_view switch selects a newer Twig detail; it is not the nextgen API.
| Repository / source | Responsibility |
|---|---|
legacy src/Company/FormationBundle/Controller/InterneController.php | HR list/detail, create/edit, course skill management, exports and related sessions/requests. |
legacy src/Customer/FormationBundle/Controller/{Interne,Search,Favorites,Requests}Controller.php | Employee landing/detail, search page, favorites and requests. |
legacy src/Main/ConfigBundle/Controller/FormationController.php | Shared administrative JSON search and employee rendered-result search. |
legacy src/Repository/FormationRepository.php::createFilteredFormations | Company scope and core search predicates. |
legacy src/Customer/FormationBundle/Services/{AccessGroupService,FormationService}.php | Configured audience/history and motivation-related suggestions. |
legacy src/Main/ConfigBundle/Form/Type/FormationType.php | Course form fields and company-bound options. |
legacy web/assets_search/js/SearchFormations.js | HR criteria, AJAX requests and 12-card pagination. |
legacy src/Customer/FormationBundle/Resources/views/Modern/ | Employee filters, cards, sections, detail and client interactions. |
nextgen-front …/Development/components/DevelopmentTrainingCatalogModal.vue | Review-specific all/suggested/selection catalog tabs, filters and selected training requests. |
nextgen-front …/Development/api/trainingCatalogFilters.api.ts | Employee-visible training field/option retrieval for filter controls. |
nextgen-back src/Http/Api/Routing/V1/training.yaml | Training CRUD and reviewee-scoped catalog contracts. |
nextgen-back src/Infrastructure/Persistence/Repository/Training/FormationRepository.php | Company/employee scope, history, duration and custom-field filtering. |
The frontend …/Development prefix is src/modules/ModuleConfig/Campaigns/modules/Reviews/modules/FormRender/components/renderers/Development.
Legacy request flow
HR search
Company/FormationBundle/InterneController::indexAction rejects the back-office hiding permission, loads company course skills/providers/categories/statuses and builds Smart Editor filters in bo context. The Twig page loads SearchFormations.js, which submits criterias, limit=12 and offset to route formation_search. Main/ConfigBundle/Controller/FormationController::searchAction requires AJAX and an authenticated user, selects the setting and delegates to createFilteredFormations, then applies Smart Editor criteria.
Criteria include search, organisme, status, tags, category, typef, duration and serialized swpFields. The checked HR client does not send its calculated locale field; server language handling applies instead. Do not assume the employee language selector exists in HR search.
Repository predicates combine independent groups with AND. Text searches course name, description, short description and code using LIKE/contains. Organization searches the provider name; category/status/type use exact values. Skill IDs match either the associated items skills or legacy direct skills using OR. This is not “must contain every selected skill.”
The response projection supplies card data such as IDs/title, summary, skills, provider/category, likes and formatted duration. UI resets and pagination are client behavior; clearSearch contains a slider dependency, so a universal working reset outcome is not established merely by the displayed control.
Employee search and suggestion sections
Customer/FormationBundle/InterneController::metiersAction renders the landing sections or course detail. SearchController::indexAction builds the dedicated search page with company skills/options and Smart Editor fo context. The page's form targets /plateforme/formation/search; result rendering is handled by formation_fo_search, mapped to Main/ConfigBundle/FormationController::searchFormationFO.
That search accepts query criteria, page, Smart Editor params and suggestion jobParams. It requests published courses, uses the current setting, applies selected/default course language, excludes configured attendance IDs, then merges audience and explicit company-field filters. It adds duration and suggestion predicates, sorts by course name and paginates with the controller's LIST_MAX_ITEMS_PER_PAGE. Do not assume identical population or sorting to the HR client.
Current-job/career suggestions use onlyUserSuggestions and job skill requirements. In InterneController::_getUserCareerSkillsFormations, no saved plans or no target jobs returns no career-suggestion data; the method then compares target-job skill levels with employee levels and associated course outcomes. For a rated skill, the checked query requires job-required level at least the employee level and course-acquired level at least the employee level. A required skill missing from the employee can match a course associated with that skill. Across career jobs, retrieveSkillsNoteFromJob keeps the highest required level. The employee career-plan detail tracks completion on individual UserPlanAction.completed actions and displays a message when all listed actions are done; UserPlan has no project-level completed field in the inspected mapping. Modern/_formation.section.html.twig renders the career empty-state when the career section has zero items; the suggestion builder uses plans, target jobs and eligible skill/course matches, not action-completion flags. Therefore, in this legacy path, the empty-state text alone does not establish an unfinished career plan. The exact behavior of the deployed workspace still requires confirmation.
Motivation suggestions use FormationService::createSuggestionByUserSkillMotivationQuery and UserskillRepository::findSkillsByMotivationLevel (default threshold >= 3), then require course outcome minus current employee level >= 0. In combined employee search, job/career and motivation predicates are ORed before the other filters. The UI groups the job and career switches as mutually exclusive where its applicable handler is installed.
These predicates explain a particular legacy search path. They are not a promise that all landing sections, LinkedIn recommendations or nextgen AI results use the same algorithm. Confluence's 2023 prose alternates “more than 3” and “>=3”; the inspected repository supports >=3 here.
Skill choices and effective search criteria
SearchController::indexAction supplies skills from FormationRepository::getAllFormationsSkills(settingId, user.locale). The SQL requires an association with a published company course and a matching skill_translation.locale. The course-language filter is separate; selecting French courses does not change the locale used to build this choices list.
Both Modern/_formation.box.html.twig and _formation_sections_paginated.html.twig::populateFormations limit cards to three skill labels and condition links on formation_hide_home_filters. A link carries tags=<skillId>, but _formation_sections_paginated.html.twig::getFilters submits selected options from #tags, rather than using the URL identifier directly. If the intended option is absent, the emitted link alone does not establish an effective skill filter.
Observed in application: the employee skill-link page had no nonblank Skills options and displayed broader course results. This establishes the missing-choice case, without proving that every skill link fails. Diagnose published associations and user-locale translations before interpreting the results as skill-specific.
Detail navigation and saved actions
Modern/_formation.box.html.twig selects an internal route or a course external URL depending on always_display_course_detailed_view. The inspected employee result linked externally; HR linked to internal detail. The internal employee controller checks an existing request and adjusts available request actions based on active/waiting/finished/refused state. This is separate from the title destination.
The internal employee detail and the card’s title destination are distinct. Observed in application: a published course’s internal route rendered Training, Skills and Useful information while its catalog title pointed to a provider. In the detail template, each skill status compares userSkill.note >= skill.note. A green acquired label reflects that recorded-level comparison, rather than course completion.
In metiersAction, request availability is derived from the existing request status. Modern/view_formation.html.twig renders an external Access the training action when a course URL exists; the alternative request button requires allowrequest == 1, all required skill levels and bo_formation_request. A waiting employee answer has accept/refuse actions, while an active request prevents a new request action. These are code-defined rendering conditions; request submission and approval outcomes remain outside the observed read-only flow.
FavoritesController::manageAction accepts JSON with formation and operation (add versus removal), updates the current user's relation, flushes and emits FormationAddFavoriteEvent / FormationRemoveFavoriteEvent. Its response is {"status":true|false}. Like/dislike uses a different controller action and feeds different presentation/popularity behavior. A successful favorite update proves neither enrollment nor completion.
Catalog request creation and follow-up
Verified in code: Customer/FormationBundle/Controller/InterneController::addAction, Main/ConfigBundle/Services/FormationRequest/FormationRequestService.php, Modern/view_formation.html.twig, RequestsController::listAction and Modern/requests_new.html.twig.
| Contract | Behavior |
|---|---|
POST /plateforme/formation/add | Legacy AJAX route; the detail client submits form data containing formation. The controller resolves a course in the current user’s company and the employee Customer record. |
| Initial request status | FormationRequestService::with requires a company request status with is_active=true and default_value=true. Absence raises an error before creation. This is independent of the course’s publication status. |
| Creation | createRequest sets user, course, status, title and creation date, persists and flushes, then invokes request-history creation. |
| Notification and event | sendNotificationEmail invokes MailerHandler with TRAINING_REQUEST and the employee’s manager User as context. FormationPostulateEvent follows. Actual recipients and delivery depend on notification configuration. |
| Response | Success returns {"response":1}. A caught exception dispatches PostCatchEvent and returns HTTP 400 with {"response":"error"}. |
Persistence precedes notification delivery. A failure after the flush can coexist with a saved request; check stored requests and history before retrying. The detail client disables Send and handles the success callback, but does not define an error callback in this path. A dialog or button state is therefore not sufficient evidence of the final request outcome.
request_formation_form is consumed in Company/ProspectBundle/Resources/views/Includes/development.html.twig, around an employee-record request button that also has an inline hidden style. It is distinct from the modern standalone detail’s bo_formation_request condition. module_formation_catalogue_fo_demande hides the My requests entry in Modern/_top.page.options.html.twig; RequestsController::listAction does not repeat that navigation flag. The management-tab flag hide_formation_demands is described in the admin screen, but its rendering consumer was not located in the inspected source tree.
The newer legacy request page classifies entries as follows:
- Coming: eligible request statuses with a future session end, or qualifying requests without a session; the latter condition can include a finished request status.
- Completed: a session end in the past, excluding employee abandonment/refusal and manager/HR refusal states. The date comparison does not require
isFinished. - Refused: employee refusal/abandonment or manager/HR refusal flags.
_request_new.box.html.twig can open the provider from the card image while the text title links to the internal course detail. Request-list location, notification delivery, attendance and skill attainment remain separate facts.
Observed in application: the admin Training Management screen exposes separate Management Platform/Employee Platform sections, and the employee request page renders Coming, Completed and Refused categories and their empty-state messages. This supports navigation and page structure; request creation, mail delivery and approval outcomes remain unverified in the deployed application.
Session assignment and attendance confirmation
Verified in code: Company/FormationBundle/Resources/views/Interne/requests.html.twig renders the request assignment control only when requests are present, show_formation_session is enabled and the effective school permission is empty or contains delete_create_edit_give_sessions. Session create/edit/delete controls use that permission in the session list and session detail templates. The default permission file grants it to hrbp; it is false for admin_rh and manager. Tenant role configuration may override these defaults.
In Company/FormationBundle/Controller/InterneController::checkPresenceAction, the trainer/HR action resolves request records for selected users and a session, sets validationTrainer, persists the records and sends an attendance-confirmation email to each selected user's address. The email contains yes/no response links. In Customer/FormationBundle/Controller/InterneController::checkPresenceAction, the employee route verifies that the request belongs to the current user, sets isParticipated from the response and records participationDate once; a later response returns a warning rather than overwriting it. This is a notification and data mutation workflow, not a read-only step.
The employee request page's Completed category is computed from session end date and exclusion statuses; it does not require the attendance response or isFinished. Attendance response, request categorization, external provider completion and skill acquisition are distinct states.
Data model and administration
Legacy src/Main/ConfigBundle/Resources/config/doctrine/Formation.orm.yml maps formation with name, summaries/program/prerequisites/objectives, language, optional URL/duration/unit/type/currency/location, provider/category/status, skill associations and related content. Course skills with levels use Formationskill; direct skill relations also remain in the search query. Smart Editor values are stored through Formation-specific company-field entities. Requests, request statuses, sessions, attendance and favorites are separate relationships/concepts.
FormationType requires name, language and status; Organization becomes required when formation_show_organisme_field is active. Languages/providers/categories/statuses come from company definitions. Legacy Formation defaults locale to fr, independently of UI language. The who field (For whom / Pour qui) is a free-text TextareaType; it describes the intended audience. The access-group service derives restrictions from flagged User/Formation company fields, rather than that description. Optional price/diploma/rhythm/opportunity fields are added/removed according to feature controls and templates.
The admin detail controller calls CompanyEntityItemManager::getAllFormationData($formation, true) for the Useful information tab. The true argument limits the list to visible Formation company fields; the manager includes configured fields even when the course has no saved item for them. The template renders a missing field value as Not defined and renders Url/Url_i18n field values as links that open in another tab. These display rules do not establish that any particular tenant field is populated.
InterneController::newAction handles the form, sets the company, separately reads short description, validates/uploads media and persists course/documents/Smart Editor values. editAction additionally checks modify_training. Loading the legacy edit route has a write side effect: before rendering the form, it strips <span class="no-translate"> wrappers and leading whitespace from the short description, then persists and flushes the course even on GET. The edit page is therefore not a read-only inspection route. Its delete action removes the course plus requests, favorites, skills/relations, files, company-field values, sessions/session files and likes; recovery is not provided by this path. Course creation, skill links, documents and completion-driven skill updates should not be described as one atomic catalog transaction. Use the dedicated Formations/Formations&Skills adapters for the Import Center.
No automatic synchronization precedence between manual edits and source feeds, rollback across requests or retention policy was established. Treat the external source-of-truth decision as an integration contract.
Access-group and training-history semantics
Legacy AccessGroupService uses flags training_visibilty (the spelling in code) and training_attendance. It finds corresponding company User/Formation fields, matches access-group names and builds a field filter with or_null, admitting unrestricted courses as well as allowed groups. Missing configured fields or user group values can mean no restriction is applied. History derives excluded formation IDs from flagged formation values containing the user ID.
Nextgen TrainingAccessGroupProvider::provideScope similarly resolves eligible fields/options/names. FormationRepository::applyEmployeeScope applies access and history filters. A resolved scope with no matching option IDs admits unrestricted courses; no usable scope can leave group filtering absent. Review-specific findTrainingsForUserReview resolves the reviewee using TrainingCatalogRevieweeProvider; no reviewee yields 1=0. It applies the employee scope to the reviewee rather than the acting reviewer.
Observed in application: an unpublished course remained reachable through its internal employee link. Verified in code: the numeric-ID branch of Customer/FormationBundle/InterneController::metiersAction resolves the course and checks company ownership, but does not apply the catalog’s publication, access-group or history search predicates in that branch. Global route authorization remains a separate concern. Catalog exclusion therefore does not establish direct-link denial.
The generic nextgen query establishes company scope and employee access/history conditions; it must not be assumed to reproduce every legacy publication or language condition. Check the consuming route's explicit filters. These are discovery predicates, not proof of complete route-level authorization.
Nextgen API contracts and review consumer
config/routes.yaml mounts the V1 route definitions under /api/v1. training.yaml declares:
| Method / route | Permission / behavior |
|---|---|
GET /api/v1/training | read-training; findTrainings, default name ascending. |
GET /api/v1/training/{id} | read-training; company/current-user scoped lookup. |
POST /api/v1/training | create-training; create DTO validation. |
PUT/PATCH /api/v1/training/{id} | update-training; update DTO and repository lookup. |
DELETE /api/v1/training/{id} | delete-training; route declares soft_deletable: false. |
GET /api/v1/user-review/{userReviewId}/training | read-training; catalog scoped to the reviewee. |
GET /api/v1/user-review/{userReviewId}/training/{id} | Same scope for one course. |
Illustrative read-only query:
GET /api/v1/training?filters[name][contain]=Excel&filters[duration][gte]=4&filters[duration][lte]=16&sorting[name]=asc
Filter definitions cover name/locale/type string operators, numeric duration comparisons and dynamic Smart Editor fields. Multiple custom fields combine with AND; option values can be pipe-separated for multi-value equality according to field support. Invalid operators for a custom-field type are ignored by the declared contract, so a sent parameter alone does not prove a restriction was applied.
Domain/V1/Training/DTO/TrainingDTO.php requires name for writes and locale/code for creation, with uniqueness on locale+code using a company-scoped repository. Name/code lengths are bounded at 255; description HTML at 50,000. Duration, unit, type and location are detailed read-only fields in this DTO. The legacy form is not an identical create/update schema. Do not send a legacy form body as a nextgen API payload.
The review modal computes /api/v1/user-review/{id}/training. Its dynamic filters use GET /api/v1/custom-field/training-filter and /custom-field/{fieldId}/option. trainingCatalogFilters.api.ts fetches a first page of up to 50 fields and 100 options per field, suppresses toasts on errors and returns no filter choices on failed retrieval. Missing dropdowns can therefore reflect loading/error/cap behavior, not the absence of configuration.
development.utils.ts::TRAINING_DURATION_FILTERS maps Short to <4h, Medium to 4–16h inclusive, Long to >16h. These are different from the standalone legacy captions. The modal's all/suggested/selection tabs and save event belong to the review development form; their deployed availability is outside the observed standalone catalog scope.
Duration differences
| Path | Checked interpretation |
|---|---|
| Legacy HR repository | <=3 hours; <=48 hours or <=2 days; >=48 hours / >=2 days / month units for the long bucket. |
Legacy employee setDuration | Inclusive thresholds, with hour/day predicates using 24 hours per day in the applicable ranges. Short buckets only target hour units. Mutable range bounds can carry between multiple selected buckets. |
Nextgen DurationType::hours / applyDurationFilter | Hour=1, day=7, month=147. Numeric comparisons normalized to hours; absent/empty duration excluded when a numeric duration filter is applied. |
Preserve duration units and verify the actual consumer. Do not silently treat “day” as the same number of hours across generations.
AI and integration boundary
Nextgen src/Infrastructure/Persistence/Service/Development/TrainingSuggestionProvider.php calls AIHub matching/rationale, excludes already requested/history IDs, caches responses and rechecks returned IDs against requestable courses for the reviewee. The inspected caller expects a flat list of catalog identifiers and raises a development-suggestion exception on upstream transport failure. The caller contract does not establish the deployed provider’s ranking or availability.
AIHub src/matching/training/service.py exposes search_accessible_training_ids and get_user_training_rationale; these are a separate capability from deterministic legacy catalog search. The full AIHub HTTP/authentication and ranking chain was not traced here; caller expectations do not establish the callee's deployed contract. Legacy src/Service/MessageBroker/Formatter/FormationFormatter.php formats training identity/company messages for integration processing, but the formatter alone does not prove runtime delivery or refresh latency.
External LMS links, LinkedIn connectors and learning-hub destinations are configuration-dependent. Title navigation alone does not supply a completion callback; confirm the integration contract for attendance and skill updates.
Permissions, failures and observability
Legacy HR hiding/editing permissions and feature controls are separate from nextgen CRUD permissions. Employee detail checks company ownership for company-bound records; not every favorites/session/mutation path repeats the same local checks. Global security configuration was not exhaustively audited. Do not infer a universal authorization/CSRF policy from the menu or a single form.
| Failure or difference | Inspection point |
|---|---|
| Empty employee results | Publication/language, selected filters, access-group/history and usable profile/job/career inputs. |
| HR/employee counts differ | Distinct publication, locale, audience and history predicates. |
| Missing custom filters in review | Field context, request errors, first-page limits and options availability. |
| Unexpected duration | Generation-specific thresholds/unit conversion; multi-range legacy bound handling. |
| Card bypasses internal details | Course URL plus always_display_course_detailed_view. |
| Save rejected | Form/DTO required fields, status/provider choices, validation and media handling. |
| Favorite fails | status:false response after an exception; do not infer success from a heart alone. |
| Suggestions unavailable | Nextgen provider exception/cache/requestability versus deterministic legacy prerequisite rules. |
Opening legacy detail emits FormationOpenEvent; favorite changes emit dedicated events. Request/session handling can send notifications and change skills on completion. Monitoring dashboards, retry policies, feed freshness and completion feedback from external LMSs were not verified.
Runtime dependencies include Symfony/Doctrine, company configuration/Smart Editor, jQuery/Select2/Switchery for legacy, Vue/Mk4Http and API permissions for the nextgen modal, media handling for course assets, and configured external services only where activated. Extend the appropriate form/DTO, entity mapping, filter context, template/modal and tests together; adding a label alone does not implement a cross-generation field.
Evidence and limitations
| Evidence | Coverage |
|---|---|
| Verified in code | Legacy course forms, search controllers/repository, card templates, skill associations, access groups and favorites; nextgen review catalog, training API and employee-scoped repository. Source paths and symbols appear in the sections above. |
| Observed in application | Separate administrator and employee catalogs, saved course content and acquired-skill levels, published-course discovery, skill links, search/reset, favorites and external title navigation. Real screenshots appear in the functional and configuration guides. |
| Documentary background | Training Matching playbook, Training configurations, Suggestion rules and Training matching use cases. These describe intended use; repository contracts and the deployed application determine actual behavior. |
| Not verified in the deployed application | Persisted prerequisite effects and removal, access for other employee groups and all route variants, request/approval/completion flows, relevance feedback, exports, external LMS completion and the nextgen review catalog. |
Repository evidence does not establish the deployed revision, database topology, integration freshness or universal authorization behavior. The reference application observations were collected in October 2026.
Functional-guide evidence
The following distinctions underpin the functional guide:
| Product behavior | Evidence and scope |
|---|---|
| Course content language is independent of interface language. | Observed in application and Verified in code in FormationType and the course entity defaults. |
| An employee card can show a subset of course skills. | Verified in code: Modern/_formation.box.html.twig limits display to three skills. Observed in application: the management-course card shows three labels while its administrative Skills tab lists eight outcomes. |
| Skill links open Search, subject to available choices. | Verified in code: card links carry a skill ID, while the search request reads selected Skills options. Observed in application: an empty option list produced broader results. The link alone does not establish filtering. |
| Skills describe prerequisites or learning outcomes. | Verified in code: view_formation.html.twig separates required and acquired relations and uses selected-row dialogs for level changes; InterneController::addFormationSkillAction stores relation type and note. For acquired, it can call assignSkillToUsersOfFormation for users associated with the course; the import service preserves an existing nonzero level. The remove action deletes the course-skill relation only. Saved acquired-skill levels were Observed in application. This does not establish employee attainment or deployed user impact. |
| Publication and company access settings affect discovery. | Observed in application: an unpublished administrative course was absent from employee search while a published course was visible. Verified in code: publication, locale, access-group and history predicates are applied in the relevant legacy search path. The same employee could open an unpublished course by direct link. Discovery filters do not establish direct-link authorization. |
| Favorites, feedback and training requests are distinct. | Verified in code: separate controller actions and relationships; favorite save/removal were Observed in application. |
| A course title can open an external destination. | Observed in application and Verified in code in the card/detail navigation. Provider availability and completion are external integration dependencies. |
Last updated on 3 November 2026