Comment Integration
Audience: Site builders enabling commenting on contacts, and developers extending or customizing the CRM comment integration. For background on the contact entity itself, see Contact entity.
The CRM module provides optional integration with Drupal core's Comment module. When Comment is enabled alongside CRM, each contact type receives a dedicated Comments tab where internal notes can be posted against a contact record.
The Comment module is not required. CRM works fully without it; the comment integration activates automatically when both modules are enabled.
Requirements
- CRM module enabled.
- Drupal core Comment module enabled
(
Admin > Extend, ordrush pm:enable comment). - Users must have the
view any crm contactpermission (or a bundle-specific variant) and the coreaccess commentspermission to reach the Comments page.
What You Get
Comments tab on contact pages
A Comments local task appears on every contact canonical page
(/crm/contact/{id}). The tab label counts published comments:
| Comment count | Tab label |
|---|---|
| 0 | Comments (0) |
| 1 | Comment (1) |
| 2+ | Comments (N) |
The tab is hidden automatically when Comment is not enabled or when the contact type does not have a comment field.
Comments page
The tab links to /crm/contact/{crm_contact}/comment, which renders the
contact's comment thread followed by the new-comment form. The page title reads
"Comments about {contact name}".
New-comment form with destination
The comment submission form's action URL is rewritten to include:
?destination={current_path}— returns the user to the Comments page after posting.&crm_contact={id}— identifies the contact for downstream processing.
Edit and delete links on individual comments also carry a ?destination query
parameter so that post-action redirects return to the same CRM contact comment
page.
Configuration
The integration is driven entirely by optional configuration that CRM installs automatically when Comment is enabled. No manual configuration steps are needed.
Installed optional configuration
| Config object | Purpose |
|---|---|
comment.type.crm_contact |
Defines the crm_contact comment type targeting the crm_contact entity. |
field.storage.crm_contact.comment |
Shared field storage for the comment field on crm_contact. |
field.field.crm_contact.person.comment |
Attaches the comment field to the Person bundle. |
field.field.crm_contact.organization.comment |
Attaches the comment field to the Organization bundle. |
field.field.crm_contact.household.comment |
Attaches the comment field to the Household bundle. |
core.entity_view_mode.crm_contact.comment |
Registers the comment view mode on crm_contact. |
core.entity_view_display.crm_contact.{bundle}.comment |
View display (one per bundle) used when rendering the comment thread. |
core.entity_view_display.comment.crm_contact.default |
Default view display for crm_contact comment entities. |
core.entity_form_display.comment.crm_contact.default |
Default form display for new crm_contact comments. |
field.field.comment.crm_contact.comment_body |
The body field on crm_contact comment entities. |
How optional config is installed
InstallOptionalConfigHooks::modulesInstalled() listens to
hook_modules_installed. When comment appears in the list of newly enabled
modules, it reads CRM's config/optional directory and calls
ConfigInstallerInterface::installOptionalConfig() scoped to configs that
declare comment as a dependency. This ensures no config objects land before
their dependencies are satisfied.
Adding the comment field to a custom contact type
If you define a new contact type bundle, you can attach the comment field to it
through the Field UI (Admin > Structure > CRM > Contact types > {type} >
Manage fields) or programmatically:
use Drupal\field\Entity\FieldConfig;
FieldConfig::create([
'field_name' => 'comment',
'entity_type' => 'crm_contact',
'bundle' => 'my_type',
'label' => 'Comment',
'default_value' => [[
'status' => 2,
'cid' => 0,
'last_comment_timestamp' => 0,
'last_comment_name' => NULL,
'last_comment_uid' => 0,
'comment_count' => 0,
]],
])->save();
Enabling Comment alongside CRM
# Using Drush
ddev drush pm:enable comment -y
Clear caches after enabling:
ddev drush cache:rebuild
No additional configuration is required. The comment type, fields, and displays are installed automatically.
Permissions
| Permission | Effect |
|---|---|
access comments |
Allows users to view comment threads and access the Comments page. |
post comments |
Allows users to post new comments on contacts. |
administer comments |
Allows editing and deleting all comments. |
view any crm contact |
Required for the access check in CommentController::access(). |
Users who have access comments but lack contact view access will receive a
forbidden response on the Comments page.
Troubleshooting
Comments tab does not appear
- Verify Comment is enabled — Go to
Admin > Extendand confirm the Comment module is checked. - Check the comment field — The contact type must have a
commentfield instance. Confirm it exists atAdmin > Structure > CRM > Contact types > {type} > Manage fields. - Clear caches — Run
ddev drush cache:rebuildafter enabling Comment. - Check permissions — The user needs
access commentsin addition to contact view access.
Access denied on the Comments page
The CommentController::access() method returns forbidden when:
- The Comment module is not enabled.
- The contact type does not have a
commentfield. - The current user does not have
access comments. - The current user cannot view the contact.
Review all four conditions if you receive a 403.
Comment form redirects to the wrong page after posting
The crm.comment_lazy_builders service rewrites the form #action to append
destination and crm_contact query parameters. If the redirect lands
elsewhere:
- Confirm the service is registered — check
drush container:debug | grep crm.comment_lazy_builders. - Confirm
CommentController::commentsPage()is substituting the#lazy_buildercallback for the form element. - Rebuild the container:
ddev drush cache:rebuild.
Comment count in the tab is stale
The CommentTask local task is cached with the crm_contact:{id} cache tag.
Posting or deleting a comment should invalidate it automatically. If the count
is stale:
- Run
ddev drush cache:rebuildto force-clear all caches. - Confirm that comment saves are triggering cache-tag invalidation for the
crm_contactentity.
Implementation details
Audience: Module developers and contributors.
Route
crm.contact_comment:
path: '/crm/contact/{crm_contact}/comment'
controller: CommentController::commentsPage
title callback: CommentController::title
access: CommentController::access
The {crm_contact} route parameter is upcasted to a ContactInterface entity.
CommentController
Class: Drupal\crm\Controller\CommentController
access()— verifies Comment is installed, the contact has acommentfield, and the user has bothviewaccess on the entity and theaccess commentspermission.commentsPage()— renders the contact'scommentfield in thecommentview mode, then replaces the core#lazy_builderon the form element with the CRM-awarecrm.comment_lazy_builders:renderForm.title()— returns a translatable title containing the contact label.
CommentTask local task
Class: Drupal\crm\Plugin\Menu\LocalTask\CommentTask
Extends LocalTaskDefault. Its getTitle() method:
- Returns the plain
Commentslabel when Comment is not enabled or the contact has nocommentfield. - Queries published comments scoped to the current contact and field name.
- Returns a pluralized label:
Comment (1)orComments (N).
getCacheTags() returns ['crm_contact:{id}'] so the badge invalidates
whenever the contact entity changes.
crm.comment_lazy_builders service
Class: Drupal\crm\Service\CommentLazyBuilders
Registered in crm.services.yml using an optional service reference:
crm.comment_lazy_builders:
class: Drupal\crm\Service\CommentLazyBuilders
autowire: false
arguments: ['@path.current']
calls:
- [setCommentLazyBuilders, ['@?comment.lazy_builders']]
The @? prefix makes comment.lazy_builders optional — when Comment is not
installed, setCommentLazyBuilders(null) is called and renderForm() returns
[]. When Comment is installed, renderForm() delegates to the core service
and appends destination and crm_contact parameters to #action.
CommentHooks
Class: Drupal\crm\Hook\CommentHooks
Implements hook_comment_links_alter. For comments attached to a crm_contact
entity, while on the crm.contact_comment route, it appends a destination
query parameter to every link URL so that edit/delete/reply actions return the
user to the same CRM comment page.
Architecture overview
flowchart TD
subgraph config [Optional config "config/optional"]
CommentType["comment.type.crm_contact"]
FieldStorage["field.storage.crm_contact.comment"]
FieldInstances["field.field.crm_contact.{bundle}.comment"]
Displays["View + form displays"]
end
subgraph runtime [Runtime]
Tab["CommentTask\n(local task tab)"]
Route["crm.contact_comment\n/crm/contact/{id}/comment"]
Controller["CommentController"]
LazyBuilders["crm.comment_lazy_builders"]
Hook["CommentHooks\nhook_comment_links_alter"]
end
CommentType --> FieldStorage
FieldStorage --> FieldInstances
FieldInstances --> Tab
FieldInstances --> Controller
Tab -->|"links to"| Route
Route --> Controller
Controller -->|"renders field\nview mode: comment"| Displays
Controller -->|"injects lazy builder"| LazyBuilders
LazyBuilders -->|"wraps core\ncomment.lazy_builders"| Hook
Testing
| Test class | Type | What it covers |
|---|---|---|
Drupal\Tests\crm\Functional\Plugin\Menu\LocalTask\CommentsTest |
Functional | Tab label counts (0, 1, N) against real contact + comment entities |
Drupal\Tests\crm\Kernel\Controller\CommentControllerTest |
Kernel | Render array structure, title, graceful handling of missing field |
Drupal\Tests\crm\Unit\Controller\CommentControllerTest |
Unit | access() logic, controller construction |
Drupal\Tests\crm\Unit\Service\CommentLazyBuildersTest |
Unit | renderForm() with/without inner service, #action rewrite, argument pass-through |
Drupal\Tests\crm\Unit\Plugin\Menu\LocalTask\CommentTaskTest |
Unit | Title logic, cache tags |
Drupal\Tests\crm\Kernel\Plugin\Menu\LocalTask\CommentTaskTest |
Kernel | Plugin discovery and count query |
Drupal\Tests\crm\Unit\Hook\CommentHooksTest |
Unit | Link alteration and destination injection |
Drupal\Tests\crm\Kernel\Hook\InstallOptionalConfigHooksTest |
Kernel | Optional config installed on comment module enable |
To run the comment-specific tests in isolation:
ddev phpunit --filter=CommentsTest
ddev phpunit --filter=CommentControllerTest
ddev phpunit --filter=CommentLazyBuildersTest
ddev phpunit --filter=CommentTaskTest
ddev phpunit --filter=CommentHooksTest
Related documentation
- Contact entity — contact fields and bundles
- Navigation integration — contact tabs in the Navigation toolbar, including the Comments route
- Views integration — exposing CRM contact data in Views