Skip to content

Newsletter Module Decoupling Report

Current State

Newsletter is a 96-file module that only 3 external modules depend on. The coupling is concentrated almost entirely in the Customer module.

Newsletter's Own Dependencies

  • composer.json requires: Backend, Cms, Customer, Eav, Email, RequireJs, Store, Widget, Ui
  • module.xml sequence: Store, Customer, Eav, Widget
  • Newsletter → Customer is correct (newsletter needs customers)

External Modules Depending on Newsletter

Module composer.json PHP imports XML Severity
Customer hard require 11 files layout, templates, UI component Heavy
CustomerGraphQl hard require + sequence 3 files Moderate
Review hard require 1 file (inherited constructor param) Phantom

The Problem: Customer → Newsletter Coupling

The coupling is bidirectional: - Newsletter → Customer (correct): Newsletter needs Customer to manage subscriptions - Customer → Newsletter (wrong): Customer admin/frontend blocks directly import Newsletter models

Customer Module's 11 Newsletter Import Points

Admin blocks (4 files): - Block/Adminhtml/Edit/Tab/Newsletter.php — entire admin tab for newsletter subscription - Block/Adminhtml/Edit/Tab/Newsletter/Grid.php — queue grid in admin customer edit - Block/Adminhtml/Edit/Tab/Newsletter/Grid/Filter/Status.php — status filter

Frontend blocks (3 files): - Block/Form/Register.php — uses Newsletter\Model\Config to check if newsletter enabled - Block/Account/Dashboard.php — uses Subscriber, SubscriberFactory - Block/Account/Dashboard/Info.php — uses Subscriber, SubscriberFactory

Admin controllers (4 files): - Controller/Adminhtml/Index/Save.phpSubscriberFactory, SubscriptionManagerInterface - Controller/Adminhtml/Index/MassSubscribe.phpSubscriptionManagerInterface - Controller/Adminhtml/Index/MassUnsubscribe.phpSubscriptionManagerInterface - Controller/Adminhtml/Index/Cart.phpSubscriberFactory

Frontend controller (1 file): - Controller/Account/CreatePost.phpSubscriberFactory

Templates (2 files): - view/frontend/templates/form/newsletter.phtml — subscribe checkbox on registration - view/adminhtml/templates/tab/newsletter.phtml — admin newsletter tab

Layout (1 file): - view/adminhtml/layout/customer_index_newsletter.xml — admin newsletter handle

Newsletter's Plugin INTO Customer (the reverse hook)

Newsletter/Model/Plugin/CustomerPlugin.php hooks into CustomerRepositoryInterface: - afterSave() — syncs subscription when customer is saved - aroundDeleteById() — deletes subscriptions when customer is deleted - afterDelete() — same, different delete path - afterGetById() — adds is_subscribed extension attribute - afterGetList() — adds is_subscribed to customer list results

Newsletter/etc/extension_attributes.xml adds is_subscribed boolean to CustomerInterface.


Decoupling Plan

Phase 1: Break Customer → Newsletter (Move newsletter UI to Newsletter module)

The Customer module should not import anything from Newsletter. All newsletter-related UI that currently lives in Customer should move to Newsletter (where it belongs).

Move FROM Customer/ TO Newsletter/:

Current Location (Customer) New Location (Newsletter)
Block/Adminhtml/Edit/Tab/Newsletter.php Block/Adminhtml/Customer/Tab/Newsletter.php
Block/Adminhtml/Edit/Tab/Newsletter/Grid.php Block/Adminhtml/Customer/Tab/Newsletter/Grid.php
Block/Adminhtml/Edit/Tab/Newsletter/Grid/Filter/Status.php Block/Adminhtml/Customer/Tab/Newsletter/Grid/Filter/Status.php
view/adminhtml/templates/tab/newsletter.phtml Keep in Newsletter (already has admin templates)
view/adminhtml/layout/customer_index_newsletter.xml Newsletter/view/adminhtml/layout/customer_index_newsletter.xml

Refactor in Customer module (make Newsletter optional):

File Change
Block/Form/Register.php Remove Newsletter\Model\Config import. Use a config value check instead (config/newsletter/active) or make the newsletter form block a separate child block contributed by Newsletter module via layout XML
Block/Account/Dashboard.php Remove SubscriberFactory from constructor. Newsletter module contributes subscription status via its existing plugin on CustomerRepositoryInterface (the is_subscribed extension attribute)
Block/Account/Dashboard/Info.php Same — read is_subscribed from customer extension attributes instead of direct Subscriber model
Controller/Account/CreatePost.php Remove SubscriberFactory. Dispatch an event (customer_register_success already exists) — Newsletter observes it and handles subscription
Controller/Adminhtml/Index/Save.php Remove SubscriberFactory and SubscriptionManagerInterface. Newsletter's existing CustomerPlugin::afterSave() already handles this via the is_subscribed extension attribute
Controller/Adminhtml/Index/MassSubscribe.php Move to Newsletter module as Newsletter/Controller/Adminhtml/Customer/MassSubscribe.php
Controller/Adminhtml/Index/MassUnsubscribe.php Move to Newsletter module as Newsletter/Controller/Adminhtml/Customer/MassUnsubscribe.php
Controller/Adminhtml/Index/Cart.php Remove SubscriberFactory — it's only passed to parent constructor, likely unused here

Frontend templates: - form/newsletter.phtml — move to Newsletter module, contribute via layout XML handle customer_account_create - account/dashboard/info.phtml — the newsletter status line can read from customer's is_subscribed extension attribute (set by Newsletter's plugin) instead of directly loading Subscriber model

Phase 2: Clean Up composer.json Dependencies

After Phase 1:

Module Change
Customer/composer.json Remove magento/module-newsletter from require
CustomerGraphQl/composer.json Move magento/module-newsletter from require to suggest; make subscription logic conditional
Review/composer.json Remove magento/module-newsletter from require (phantom — only inherited constructor param)

Phase 3: Fix Review's Inherited Dependency

Review/Block/Customer/ListCustomer.php extends Customer/Block/Account/Dashboard which currently takes SubscriberFactory in its constructor. After Phase 1 removes SubscriberFactory from Dashboard's constructor, Review's dependency disappears automatically.


How Newsletter Already Does It Right

Newsletter's CustomerPlugin is actually the correct pattern — it hooks into CustomerRepository via plugins and extension attributes. The problem is that the Customer module ALSO reaches back into Newsletter directly, creating the circular dependency.

After decoupling: - Newsletter → Customer: via plugin on CustomerRepositoryInterface (keeps working) - Newsletter → Customer UI: newsletter admin tabs, registration checkbox, dashboard status — all contributed by Newsletter module via layout XML - Customer → Newsletter: zero dependency — Customer only sees the is_subscribed extension attribute, which is framework-level (extension_attributes.xml)

Architecture After Decoupling

Customer module (no Newsletter imports)
    ├── extension_attributes: is_subscribed (boolean)  ← set by Newsletter plugin
    ├── events: customer_register_success              ← observed by Newsletter
    └── layout handles: customer_account_create etc.   ← Newsletter adds blocks via XML

Newsletter module (depends on Customer)
    ├── Plugin/CustomerPlugin.php   → hooks CustomerRepositoryInterface
    ├── Observer/CustomerRegister   → handles subscription on registration
    ├── Block/Adminhtml/Customer/*  → admin UI (moved from Customer)
    ├── layout XML contributions    → adds newsletter blocks to Customer pages
    └── extension_attributes.xml    → declares is_subscribed on CustomerInterface

If Newsletter is disabled/removed: - is_subscribed extension attribute simply doesn't exist - No newsletter blocks appear on registration or dashboard - No newsletter admin tab - Customer module works perfectly without it


Difficulty Assessment

Phase Effort Risk
Phase 1: Move blocks/controllers Medium Low — layout XML handles stay the same, just different module provides them
Phase 2: Remove composer deps Low Low — after Phase 1, no code references remain
Phase 3: Review cleanup Trivial Zero — automatic after Dashboard constructor changes

Total: Medium effort, low risk, high value — makes Newsletter fully optional.