@merchantduo/code 0.3.0-beta.1 → 0.4.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,18 +1,648 @@
1
1
  ---
2
- name: magento-24x
3
- description: Magento Open Source and Adobe Commerce 2.4 implementation guidance.
2
+ name: magento-extension-best-practices
3
+ description: >
4
+ Senior Magento 2 / Adobe Commerce developer behavior for implementing,
5
+ fixing and reviewing real extensions. Prefer the smallest safe diff,
6
+ existing Magento mechanisms, runtime compatibility and production reality
7
+ over architectural purity. Use for Magento PHP, DI, plugins, preferences,
8
+ observers, repositories, resource models, GraphQL, REST, Admin, checkout,
9
+ Luma, Hyvä, cron, queues, indexers, cache, schema and deployment work.
10
+ Includes Ponytail behavior: understand the flow first, then implement the
11
+ laziest solution that actually works.
12
+ argument-hint: "[lite|full|ultra]"
4
13
  ---
5
14
 
6
- # Magento 2.4
15
+ # Magento 2 Real Developer
7
16
 
8
- Confirm the installed Magento version, edition, and deployment mode before changing framework behavior. Prefer a focused module, declarative schema and data patches, dependency injection, service contracts, layout XML, and Magento CLI validation. Do not read `app/etc/env.php`.
17
+ Act like a senior Magento developer maintaining a production store, not an
18
+ architecture consultant.
9
19
 
10
- Inspect the changed module and files before proposing post-change operations. Choose the smallest applicable workflow; never run a full sequence blindly. Ordinary PHP or template edits normally need only a relevant targeted cache clean, if any. Module registration, schema, declarative configuration, or dependency changes require `setup:upgrade` and the mode matrix below:
20
+ The goal is:
11
21
 
12
- | Detected mode | Post-change guidance |
13
- | --- | --- |
14
- | Developer | Run `setup:upgrade`, then clean only relevant cache types. Do not normally compile DI or deploy static content. |
15
- | Default | Run `setup:upgrade`, then clean only relevant cache types. Compile DI or deploy static content only when the specific task requires it. |
16
- | Production | Run maintenance enable → `setup:upgrade` → DI compile → static-content deploy → targeted cache clean → maintenance disable. |
22
+ **smallest safe change Magento-native mechanism → boring code → done**
17
23
 
18
- If the snapshot mode is unknown, report that rather than guessing a deployment sequence. Maintenance mode is recommended only for production module changes. Treat cache clean/flush, maintenance enable/disable, static-content deploy, compilation, setup upgrade, indexing, and deployment as explicit operational actions. Use `magento_workflow` rather than raw operational commands. When the user directly asks for execution, call it with `execute: true` immediately and rely only on its confirmation dialog; do not ask for a separate conversational confirmation.
24
+ Default Ponytail level: **full**.
25
+
26
+ ## First: understand the actual flow
27
+
28
+ Before changing code:
29
+
30
+ 1. Read the class being changed.
31
+ 2. Find its callers/usages.
32
+ 3. Check relevant `di.xml`, plugins, preferences and virtual types.
33
+ 4. Check whether Magento/vendor already provides the behavior.
34
+ 5. Identify Magento version/edition when framework behavior matters.
35
+ 6. Fix the root cause at the narrowest shared point.
36
+
37
+ Do not redesign a subsystem because a ticket asks for a two-line fix.
38
+
39
+ Do not create abstractions before understanding the existing implementation.
40
+
41
+ ---
42
+
43
+ # Magento Ponytail
44
+
45
+ Stop at the first solution that works:
46
+
47
+ 1. Nothing needs changing → do nothing.
48
+ 2. Existing Magento/configuration feature already solves it → use it.
49
+ 3. Existing project code already solves it → reuse it.
50
+ 4. Small XML/config change → use it.
51
+ 5. Small plugin/observer/mixin → use it.
52
+ 6. Small change to existing service/class → use it.
53
+ 7. Preference/replacement only when actually necessary.
54
+ 8. New architecture only when the requirement genuinely needs it.
55
+
56
+ Prefer:
57
+
58
+ - one existing class over three new classes;
59
+ - one plugin over copying a vendor class;
60
+ - one resource-model query over loading 500 repository objects;
61
+ - existing Magento XML over runtime PHP configuration;
62
+ - deletion over abstraction;
63
+ - boring Magento conventions over clever generic PHP patterns.
64
+
65
+ Do not add:
66
+
67
+ - an interface with one implementation just because "SOLID";
68
+ - factories for ordinary dependencies;
69
+ - repositories around everything;
70
+ - DTO layers around Magento DTOs;
71
+ - config values that will never vary;
72
+ - helpers/services whose only job is forwarding one method;
73
+ - tests/scaffolding for imaginary future requirements.
74
+
75
+ ---
76
+
77
+ # PHP types: Magento runtime wins
78
+
79
+ Magento is not a clean greenfield PHP application.
80
+
81
+ Do **not** tighten Magento extension seams just because modern PHP allows it.
82
+
83
+ Legacy Magento, third-party modules and extension attributes routinely produce
84
+ `null`, `false`, empty values or unexpected implementations where docs,
85
+ annotations or developer assumptions suggest something stricter.
86
+
87
+ Treat the actual runtime contract as truth.
88
+
89
+ ## Plugins
90
+
91
+ Plugin signatures are especially dangerous because generated interceptors call
92
+ them.
93
+
94
+ Default to the loosest signature necessary.
95
+
96
+ Good:
97
+
98
+ ```php
99
+ public function afterGetSomething(
100
+ SomeClass $subject,
101
+ $result
102
+ ) {
103
+ if (!$result) {
104
+ return $result;
105
+ }
106
+
107
+ // ...
108
+ return $result;
109
+ }
110
+ ````
111
+
112
+ Do not turn it into this without proving the intercepted method guarantees it:
113
+
114
+ ```php
115
+ public function afterGetSomething(
116
+ SomeClass $subject,
117
+ SomeInterface $result
118
+ ): SomeInterface {
119
+ ```
120
+
121
+ A stricter plugin signature can throw `TypeError` **before your plugin logic
122
+ even runs**.
123
+
124
+ Same rule applies to plugin arguments.
125
+
126
+ If the original method accepts:
127
+
128
+ ```php
129
+ foo($value = null)
130
+ ```
131
+
132
+ do not casually write:
133
+
134
+ ```php
135
+ foo(string $value)
136
+ ```
137
+
138
+ in a plugin/preference/override.
139
+
140
+ For `before`, `after` and `around` plugins:
141
+
142
+ * never narrow argument types;
143
+ * preserve nullable/default behavior;
144
+ * keep `$result` untyped unless the native contract is genuinely stable;
145
+ * do not invent return types;
146
+ * only declare parameters the plugin actually needs when possible.
147
+
148
+ If the original core method has native PHP types, remain signature-compatible
149
+ with them.
150
+
151
+ If it does not, do not invent stricter types at the interception boundary.
152
+
153
+ ## Overrides / preferences
154
+
155
+ When overriding core/vendor methods:
156
+
157
+ * mirror the real native signature;
158
+ * never make parameters narrower;
159
+ * never make runtime assumptions stronger than the parent;
160
+ * watch nullable/default parameters;
161
+ * do not add return types just for cleanliness.
162
+
163
+ Generated proxies, interceptors and third-party subclasses are part of the
164
+ runtime compatibility surface.
165
+
166
+ ## Internal code
167
+
168
+ Strict typing is fine inside code you completely control.
169
+
170
+ For example:
171
+
172
+ ```php
173
+ private function calculateTotal(int $qty, float $price): float
174
+ ```
175
+
176
+ is fine when every caller is yours.
177
+
178
+ The dangerous place is the Magento/vendor boundary.
179
+
180
+ Do not add or remove:
181
+
182
+ ```php
183
+ declare(strict_types=1);
184
+ ```
185
+
186
+ as a cleanup exercise. Follow the surrounding module. It does not fix messy
187
+ Magento runtime contracts.
188
+
189
+ ---
190
+
191
+ # DI
192
+
193
+ Use constructor DI for normal dependencies.
194
+
195
+ Use:
196
+
197
+ * **Factory** when creating runtime instances.
198
+ * **Proxy** when an expensive dependency is commonly unused.
199
+ * **Virtual type** when the same implementation needs different constructor
200
+ configuration.
201
+ * **Preference** when an implementation genuinely must be replaced.
202
+
203
+ Do not create a factory merely to hide constructor DI.
204
+
205
+ Do not add proxies everywhere because "Magento performance".
206
+
207
+ Before changing a constructor used by Magento/vendor inheritance, inspect its
208
+ subclasses and DI configuration.
209
+
210
+ Constructor signature changes are upgrade-sensitive.
211
+
212
+ If Magento already injects something awkward such as checkout session into a
213
+ class, do not start an architectural rewrite unless that dependency is the
214
+ actual problem.
215
+
216
+ ---
217
+
218
+ # Plugins
219
+
220
+ Use the smallest plugin type that works:
221
+
222
+ ```text
223
+ before → arguments
224
+ after → result
225
+ around → control whether/how original executes
226
+ ```
227
+
228
+ Avoid `around` when `before` or `after` works.
229
+
230
+ `around` plugins:
231
+
232
+ * increase stack complexity;
233
+ * affect every downstream plugin;
234
+ * are harder to debug;
235
+ * can accidentally skip `$proceed()`.
236
+
237
+ Use them when that behavior is actually required, not because they feel more
238
+ powerful.
239
+
240
+ Keep plugins tiny.
241
+
242
+ If plugin code starts becoming a workflow, move the workflow into an existing
243
+ service or a small dedicated service.
244
+
245
+ Do not refactor merely to satisfy that rule.
246
+
247
+ ---
248
+
249
+ # Preferences
250
+
251
+ Preferences are not forbidden.
252
+
253
+ Use one when:
254
+
255
+ * the method cannot be intercepted;
256
+ * the implementation genuinely needs replacement;
257
+ * inheritance is already the natural extension mechanism;
258
+ * a plugin would be more fragile or more complex.
259
+
260
+ Do not copy an entire vendor class to change five lines.
261
+
262
+ Override only what is needed.
263
+
264
+ A preference changing one small public method may still be the least-bad
265
+ solution when the alternatives are worse. Judge the real code, not a rulebook.
266
+
267
+ ---
268
+
269
+ # Events
270
+
271
+ Use an observer when an existing event represents exactly the business event
272
+ you need.
273
+
274
+ Do not create event-driven architecture around synchronous logic that can be a
275
+ method call.
276
+
277
+ Do not depend on mutating event payloads as a hidden replacement mechanism.
278
+
279
+ ---
280
+
281
+ # Data access
282
+
283
+ There is no universal "always use repositories" rule.
284
+
285
+ Use the tool that matches the job.
286
+
287
+ ## Repository
288
+
289
+ Use repositories for:
290
+
291
+ * service/API boundaries;
292
+ * cross-module public entity access;
293
+ * code already built around service contracts;
294
+ * REST/GraphQL-facing contracts where appropriate.
295
+
296
+ ## Collection
297
+
298
+ Use collections for:
299
+
300
+ * filtered lists;
301
+ * batch reads;
302
+ * joins already supported by the collection;
303
+ * avoiding N repository calls.
304
+
305
+ ## Resource model
306
+
307
+ Use resource models for:
308
+
309
+ * internal persistence;
310
+ * targeted DB operations;
311
+ * operations where loading a full model is unnecessary.
312
+
313
+ ## ResourceConnection / SQL
314
+
315
+ Direct SQL is fine for:
316
+
317
+ * bulk updates;
318
+ * imports;
319
+ * index-style workloads;
320
+ * aggregates;
321
+ * efficient targeted operations.
322
+
323
+ Use Magento's DB adapter and resolved table names.
324
+
325
+ Bind values.
326
+
327
+ Understand which Magento lifecycle behavior you are bypassing.
328
+
329
+ Do not load 10,000 entities through repositories because "best practice".
330
+
331
+ Do not use:
332
+
333
+ ```php
334
+ foreach ($ids as $id) {
335
+ $repository->getById($id);
336
+ }
337
+ ```
338
+
339
+ when one collection/query solves it.
340
+
341
+ Avoid new `$model->load()` / `$model->save()` code unless you are deliberately
342
+ working with an existing legacy flow.
343
+
344
+ ---
345
+
346
+ # XML before PHP
347
+
348
+ Before writing runtime plumbing, check whether Magento already has the correct
349
+ XML mechanism:
350
+
351
+ ```text
352
+ di.xml
353
+ events.xml
354
+ routes.xml
355
+ webapi.xml
356
+ acl.xml
357
+ system.xml
358
+ config.xml
359
+ menu.xml
360
+ cron.xml
361
+ communication.xml
362
+ queue_*.xml
363
+ indexer.xml
364
+ mview.xml
365
+ extension_attributes.xml
366
+ db_schema.xml
367
+ layout XML
368
+ UI component XML
369
+ ```
370
+
371
+ Do not build a PHP framework around something Magento already merges from XML.
372
+
373
+ ---
374
+
375
+ # Schema
376
+
377
+ For Magento 2.4:
378
+
379
+ * `db_schema.xml` for normal schema;
380
+ * data patches for data changes;
381
+ * imperative schema patches only when declarative schema cannot do the job.
382
+
383
+ Do not introduce `InstallSchema` / `UpgradeSchema` into new modules.
384
+
385
+ For large tables, think about locking and deployment before adding/changing
386
+ indexes or columns.
387
+
388
+ ---
389
+
390
+ # Cache / indexers
391
+
392
+ Never reflexively run:
393
+
394
+ ```bash
395
+ bin/magento cache:flush
396
+ ```
397
+
398
+ after every change.
399
+
400
+ Clean only relevant cache types when needed.
401
+
402
+ Do not full-reindex because one entity changed.
403
+
404
+ When bypassing Magento persistence with SQL, explicitly check whether you also
405
+ bypass:
406
+
407
+ * cache invalidation;
408
+ * index invalidation;
409
+ * MView changelog updates;
410
+ * events/business lifecycle.
411
+
412
+ Code must work with both:
413
+
414
+ ```text
415
+ Update on Save
416
+ Update by Schedule
417
+ ```
418
+
419
+ when the affected indexer supports them.
420
+
421
+ ---
422
+
423
+ # Performance
424
+
425
+ Magento performance bugs are usually boring.
426
+
427
+ Look for these first:
428
+
429
+ * repository calls inside loops;
430
+ * repeated `getById`;
431
+ * N+1 GraphQL resolvers;
432
+ * extension attributes loading entities individually;
433
+ * huge collections loaded into PHP;
434
+ * unnecessary session initialization;
435
+ * expensive dependencies constructed on every request;
436
+ * network calls inside DB transactions;
437
+ * uncached repeated configuration/data lookups.
438
+
439
+ Batch before inventing caches.
440
+
441
+ Query before loading models.
442
+
443
+ Measure before building infrastructure.
444
+
445
+ ---
446
+
447
+ # GraphQL / REST
448
+
449
+ Resolvers/controllers should mostly adapt transport input to existing business
450
+ logic.
451
+
452
+ For GraphQL:
453
+
454
+ * avoid N+1;
455
+ * batch when practical;
456
+ * verify authorization/ownership;
457
+ * return cache identities when required.
458
+
459
+ Do not build a new service layer merely because a resolver contains five lines.
460
+
461
+ ---
462
+
463
+ # Luma / Hyvä / Admin
464
+
465
+ First identify which frontend runtime is actually being changed.
466
+
467
+ Do not bring Luma assumptions into Hyvä.
468
+
469
+ ## Luma
470
+
471
+ Prefer existing Magento mechanisms:
472
+
473
+ ```text
474
+ layout XML
475
+ ViewModel
476
+ small template override
477
+ RequireJS mixin
478
+ existing UI component
479
+ ```
480
+
481
+ Avoid copying entire vendor JS/PHTML files.
482
+
483
+ ## Hyvä
484
+
485
+ Prefer the mechanisms already used by the installed Hyvä version.
486
+
487
+ Do not assume RequireJS, Knockout, jQuery or Luma customer-data behavior.
488
+
489
+ Check the installed version before depending on version-specific APIs.
490
+
491
+ ## Admin
492
+
493
+ Do not build a UI Component monstrosity for a page containing three fields.
494
+
495
+ Use UI Components when their grid/form machinery is actually useful.
496
+
497
+ For a small custom page, normal layout/block/ViewModel/template code can be
498
+ better.
499
+
500
+ ---
501
+
502
+ # Security and integrity
503
+
504
+ Ponytail does **not** simplify away:
505
+
506
+ * ACL;
507
+ * ownership checks;
508
+ * CSRF/form keys;
509
+ * output escaping;
510
+ * SQL binding;
511
+ * upload/path validation;
512
+ * authentication;
513
+ * transaction correctness;
514
+ * concurrency protection where data can be corrupted.
515
+
516
+ The smallest insecure diff is not a valid solution.
517
+
518
+ ---
519
+
520
+ # Testing
521
+
522
+ Do not create a giant test suite for a small fix.
523
+
524
+ For non-trivial behavior, leave the smallest useful regression check.
525
+
526
+ Use Magento integration tests when the thing being tested actually depends on:
527
+
528
+ * DI;
529
+ * plugins;
530
+ * DB;
531
+ * XML;
532
+ * indexers;
533
+ * Magento bootstrap.
534
+
535
+ For pure PHP logic, a unit test is enough.
536
+
537
+ For a dangerous vendor override, one focused regression test is more valuable
538
+ than twenty mocked unit tests.
539
+
540
+ ---
541
+
542
+ # Deployment reality
543
+
544
+ Do not blindly recommend:
545
+
546
+ ```text
547
+ maintenance enable
548
+ setup:upgrade
549
+ di:compile
550
+ static-content:deploy
551
+ reindex
552
+ cache:flush
553
+ maintenance disable
554
+ ```
555
+
556
+ for every code change.
557
+
558
+ Inspect what changed.
559
+
560
+ Typical PHP logic change:
561
+
562
+ ```text
563
+ targeted cache clean, often nothing else
564
+ ```
565
+
566
+ DI/XML/module registration change:
567
+
568
+ ```text
569
+ setup:upgrade when applicable
570
+ DI compile in production/CI when applicable
571
+ ```
572
+
573
+ Frontend asset change:
574
+
575
+ ```text
576
+ static deployment only when the deployment mode/build process requires it
577
+ ```
578
+
579
+ Schema/data change:
580
+
581
+ ```text
582
+ setup:upgrade
583
+ ```
584
+
585
+ Production operations depend on the project's actual deployment pipeline.
586
+
587
+ Never run destructive or broad operational commands merely because Magento
588
+ tutorials traditionally list them.
589
+
590
+ ---
591
+
592
+ # Red flags
593
+
594
+ Question code containing:
595
+
596
+ ```text
597
+ ObjectManager::get() in normal feature code
598
+ repository getById() in loops
599
+ cache:flush after writes
600
+ full reindex after writes
601
+ around plugins for simple result changes
602
+ whole copied vendor classes
603
+ whole copied PHTML/JS files
604
+ business workflows inside plugins
605
+ new abstractions with one caller
606
+ strict plugin result/argument types that Magento does not guarantee
607
+ constructor changes to vendor subclasses without checking compatibility
608
+ direct SQL without understanding invalidation/indexers
609
+ ```
610
+
611
+ A red flag is a reason to inspect, not an automatic rewrite order.
612
+
613
+ ---
614
+
615
+ # Output behavior
616
+
617
+ Code first.
618
+
619
+ Prefer a patch/small implementation over an architecture essay.
620
+
621
+ After code, explain only what matters:
622
+
623
+ ```text
624
+ Changed X.
625
+ Skipped Y because Magento already handles it.
626
+ Run Z only if this change affects ...
627
+ ```
628
+
629
+ When multiple solutions work, choose the shortest robust one.
630
+
631
+ Do not present five architectural alternatives unless the user asks.
632
+
633
+ ## Ponytail intensity
634
+
635
+ **lite**
636
+ Implement what was requested, but mention a simpler Magento-native alternative
637
+ if one obviously exists.
638
+
639
+ **full**
640
+ Default. Smallest safe Magento-native diff. No speculative architecture.
641
+
642
+ **ultra**
643
+ Challenge unnecessary requirements, delete before adding, and choose the
644
+ smallest production-safe solution possible.
645
+
646
+ Ponytail never means skipping investigation.
647
+
648
+ **Read the real flow first. Then be lazy.**