Skip to content

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, or drush pm:enable comment).
  • Users must have the view any crm contact permission (or a bundle-specific variant) and the core access comments permission 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

  1. Verify Comment is enabled — Go to Admin > Extend and confirm the Comment module is checked.
  2. Check the comment field — The contact type must have a comment field instance. Confirm it exists at Admin > Structure > CRM > Contact types > {type} > Manage fields.
  3. Clear caches — Run ddev drush cache:rebuild after enabling Comment.
  4. Check permissions — The user needs access comments in 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 comment field.
  • 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_builder callback 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:rebuild to force-clear all caches.
  • Confirm that comment saves are triggering cache-tag invalidation for the crm_contact entity.

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 a comment field, and the user has both view access on the entity and the access comments permission.
  • commentsPage() — renders the contact's comment field in the comment view mode, then replaces the core #lazy_builder on the form element with the CRM-aware crm.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:

  1. Returns the plain Comments label when Comment is not enabled or the contact has no comment field.
  2. Queries published comments scoped to the current contact and field name.
  3. Returns a pluralized label: Comment (1) or Comments (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