Lately I’ve been advocating decoupling business logic from the framework (i.e. Magento) a lot.

This has multiple advantages:

  • Benefit from test driven development (TDD) without having to mock a bunch of core classes.
  • Possible reuse in different applications (for example Magento 1 and Magento 2)
  • Having separated bounded contexts helps to view parts of the domain isolated and without distraction.

Even in chirurgic modifications that we often have to do in Magento projects, it is worth identifying the actual logic and extracting it from the actual Magento classes.

Let me demonstrate it with a real-world example:

The requirement

In a custom product type that is based on bundle products, it should be possible to select an absolute fixed price as special price instead of a percentage. You should be able to choose between these two.

The product type already exists, using the unchanged price model and adminhtml blocks from the bundle type.

The solution

To encode the two different special price types in one field, I decided to save percentages as a negative value (-20 means 20 % discount). In the price model, I introduced a case differentiation in calculateSpecialPrice():

  • if the special price value is positive, use absolute special prices from the default price model
  • if it is negative, use relative special prices from the bundle price model

Additionally, I added a select box to the block for the special price input that toggles between two different text inputs.

bundle-special-price-percentage
bundle-special-price-absolute

To handle these inputs, a rewrite of the specialprice backend model was introduced

If we ignore the indexer where it is all about optimizing SQL queries, this worked and I could have been done with it. I confess to have built this with trial and error, not driven by automated tests.

But while these calculations made perfect sense for me at the time, I knew they would screw with a future programmer who might have to make changes or debug this code. I also knew that future programmer would likely be me, and I would not remember everything I had in mind that day.

So the least I could do was to collect the logic of the stored special price value at one place and document it properly.

Refactoring

Step 1: special price value objects

To do this, I first introduced a “SpecialPriceType” value object that represents “absolute” or “percentage”, and another value object “SpecialPrice”, which encapsulates original price, special price and all conversion logic from above.

Then, for example, the special price backend model would use named constructors like absolute($originalPrice, $specialPriceAbsolute) and the specialPriceValueToStore($type) method to get the value that should be saved to the database, based on the selected price type.

Step 2: Make price types intelligent

Now I had successfully moved the logic to one place independent of the Magento framework, but it still didn’t look as clear as I hoped and was a bit clumsy to be used. So I asked myself: what objects do we have and what are their responsibilities?

  • Special Price: Value object, represents an original price / special price pair
  • Special Price Type: Represents the type of the stored special price value (“absolute” or “percentage”)

It occured to me that SpecialPrice should not have to care at all about the format we use to store it in the database. So I moved the conversion from and to stored values to the SpecialPriceType class. Actually, I made two subclasses, SpecialPriceType_Absolute and SpecialPriceType_Percentage

Voilà: we isolated all the logic of how the special price is stored.

And below, the abstract base class. It has two named constructors that decide which subclass to instantiate: fromString() based on the dropdown value from the form, determineFromStoredValue() based on the stored value:

For convenience, I also kept the fromStoredValue() constructor of the SpecialPrice class but it now uses SpecialPriceType:

Result: Plug it in

And this is what calculateSpecialPrice() of the product type price model looks like now:

No more condition and the whole conversion is delegated to the framework independent code.

The same goes for the special price backend model:

Conclusion

Decoupling business logic is possible and valuable, not only in complex modules but even in small modifications. It makes the code more understandable and more testable. It might take a bit longer in the first iteration and takes some more thought, but if you ever have to deal with the code again, you will thank yourself for that.

Fabian Schmengler

Author: Fabian Schmengler

Fabian Schmengler is Magento developer and trainer at integer_net. His focus lies in backend development, conceptual design and test automation.

Fabian was repeatedly selected as a Magento Master in 2017 and 2018 based on his engagements, active participation on StackExchange and contributions to the Magento 2 core.

More Information · Twitter · GitHub