
Table of Contents:
- Using the Framework Layer Appropriately
- Designing Modules with Isolation in Mind
- Dependency Injection: How to Do It Right
- Interface Contracts and Testing Discipline
- File Structure and Readability
- Final Thoughts
Using the Framework Layer Appropriately
Magento’s Framework layer (Magento\Framework\*
) is the backbone of application services: it powers dependency injection, routing, caching, and more. But it’s not where business logic belongs.
Use the Framework layer to:
- Access infrastructure-level services: Such as file I/O, date handling, request/response objects, or cache pools.
- Integrate low-level utilities: Like
SearchCriteriaBuilder
orFilesystem
that support modular service behavior. - Extend generic behaviors: Like data serialization or HTTP cookie management.
What to avoid:
- Injecting
MessageManagerInterface
orSession
objects into service classes, which tightly couples logic to the UI layer. - Using framework classes inside domain logic without creating a service abstraction.
Why it matters: By depending directly on framework classes in business logic, you limit the portability of that code. It becomes harder to reuse in APIs, CLI tools, or background jobs, and test coverage suffers as a result.
Designing Modules with Isolation in Mind
Each Magento module should represent a focused, testable unit of behavior—not a grab bag of unrelated logic. When modules are scoped too broadly or entangled with each other, refactoring becomes risky and slow.
In a well-structured module:
- Interfaces live in the
Api/
directory, defining what the module exposes. - Data transfer objects (DTOs) live in
Api/Data/
, enabling type-safe communication. - Services are separated from delivery mechanisms (controllers, observers, plugins).
- Configuration and wiring are declared in
di.xml
,module.xml
, andetc/config.xml
, and reflect only what’s actually used.
Poorly designed modules often:
- Contain both frontend and admin logic in a single module.
- Include controller logic that performs business operations directly instead of calling services.
- Register unnecessary observers or plugins that lead to side effects across the system.
Tip for maintainability: Keep modules loosely coupled and explicitly versioned. Use sequence
in module.xml
to manage load order and ensure each module is deployable and testable on its own.
Dependency Injection: How to Do It Right
Magento’s DI container enables clean object creation, but misuse is common. Injecting too many services, relying on concrete classes, or bypassing DI entirely with ObjectManager::get()
can quickly turn a flexible system into a fragile one.
Use DI for:
- Required services used on every code path.
- Interfaces instead of concrete implementations—making it easier to mock or replace.
- Testable, predictable constructor logic free from side effects or conditional behavior.
Avoid DI when:
- Injecting expensive dependencies like full product collections that may not always be used.
- Loading optional behavior that should be lazy-loaded or determined at runtime.
- You’re tempted to inject
ObjectManager
to resolve dynamic dependencies (use factories or proxies instead).
Patterns that help:
- Proxy classes are ideal for expensive dependencies like
ProductRepository
orStockRegistry
when access is conditional. - Factory classes allow instantiation of objects that require dynamic data at runtime.
- Custom service factories can encapsulate construction logic and keep constructors lightweight.
Watch out: If a class has more than 5–7 dependencies, it's a strong sign it needs to be broken down. Class responsibilities should be narrow and clearly defined.
Interface Contracts and Testing Discipline
Good Magento code separates interface from implementation—and that separation is what enables strong, maintainable tests.
Use the Api/
directory for service contracts and the @api
annotation only when those interfaces are intended for public consumption (e.g., reusable by other modules or integrations). Avoid exposing internal logic unnecessarily.
Testing tips:
- Unit tests should cover service classes by mocking injected dependencies.
- Integration tests should verify service behavior against real database and configuration contexts.
- Keep logic out of controllers—move it to services, and test those services in isolation.
- Prefer ViewModels over helpers for data formatting in templates—they’re easier to test and maintain.
Practical example: Rather than formatting a price directly in a .phtml
file or helper, inject a ViewModel that formats based on store context and uses PriceCurrencyInterface
to remain locale-aware.
File Structure and Readability
Magento’s file structure is deep—but clean organization pays off in the long run.
Follow PSR-4 naming conventions:
- Classes in
Model/
,Service/
,ViewModel/
,Observer/
, andPlugin/
should reflect clear intent.
Group logic by responsibility, not by delivery method. For example, avoid putting data fetch logic in a controller—it belongs in a service or repository.
Avoid:
- Massive helper classes doing too many unrelated things.
- Using raw SQL in business logic—always go through
ResourceModel
andCollection
patterns. - Duplicating logic across observers, controllers, and cron jobs. Consolidate it in services and call those services from each context.
Maintain readable, self-documenting code. Favor expressive class names over comments explaining behavior. And when in doubt, add a README to each module to clarify its purpose and key dependencies.
Final Thoughts
Magento 2 gives developers a powerful architecture—but it doesn’t enforce good practices out of the box. That responsibility lies with your team.
By adhering to clear boundaries, isolating business logic, and respecting DI and modularity principles, you gain more than just cleaner code—you get a codebase that enables rapid change, safe extension, and easier onboarding.
Your future developers—and your business stakeholders—will thank you for writing Magento code that scales.