When Do Magento Block Classes Beat ViewModels?

Mark Shust explains when to use Magento ViewModels instead of legacy block classes for presentation logic. ViewModels handle data retrieval and formatting, while block classes are only needed for rare layout-system interactions like a layout-detector that adds conditional CSS classes.
Chapters
So you're ready to add some PHP code into that Magento template. Maybe you need to pull some info from the database or write some conditional logic to make it all work. But you may hit a point where you stare at your screen and wonder, where does this code actually go? Well, if you learned Magento a while back before version 2.2, you probably learned to place your code in a custom block class. If you search around Stack Overflow or ask the AI where to place your code, you'll likely get a lot of answers that still point you back to using block classes. But today that advice is pretty much all wrong because block classes in Magento are almost entirely legacy at this point. So where do you place your business logic? Let's walk through a little decision-making process that goes over a few scenarios. We'll start simple and find out if you ever need to actually use a block class. Let's get to it. Before we write out any code, let's think about how data passes around the backend to finally get rendered into a template. When you need to write PHP logic, you can place it in a few different places. Controllers, models, block classes, view models, or right inside templates. But when it comes to displaying data, you typically want to decide between the last three options, which are block classes, view models, or templates, as these deal with the presentation of data. Block classes are the old school approach that have been around since Magento 1. They can definitely do everything that view models can do, plus they have some additional functionality available to them, such as the ability to interact with the layout system access parent and child blocks, and even hook into the rendering pipeline. Placing business logic directly in template files is definitely the easiest method. Templates are part of the presentation layer of Magento and represent the V of the MVC approach of Magento's framework patterns that deal with the separation of concerns. But we've all seen that mess of spaghetti code in templates when they try to do too much. You start off adding some simple logic and it grows over time, and your template starts looking like that price.php file from Magento's Catalog module. This means that templates should really only present data and do nothing else. They can have some simple conditionals for display purposes, but they shouldn't be making any calls to the database, doing calculations, or handling any of the real backend business logic. They should be just for displaying data. That leaves us with view models. This is the newer method of dealing with complex business logic in the presentation layer, and when they were introduced in Magento 2.2, it essentially added a ViewModel layer to the framework to better handle the separation of concerns. You could think of ViewModels as kind of like block classes, but rather than having access to the layout and rendering engine, they only do one thing: pass data to templates. That's it. And by cutting out any additional functionality, it not only helps prevent convoluted practices from creeping up in your display logic, but also makes everything else much faster, since they are doing a lot less. I think all of us want a faster Magento, and using view models is a quick and easy way to push us towards achieving that goal. View models are also clean, easily testable, and should be the default choice 99% of the time when you need to decide where to place your business logic when needing to display data. Let's now see how view models actually differ from block classes. For our first scenario, let's say we just wanted to display some info on the homepage. Like the store name and phone number. Simple stuff. This is textbook ViewModel territory. We just need data, nothing fancy. We'll do this in a module named Acme Store. I already set up the registration.php and module.xml file boilerplate. Within our module, let's go ahead and create a new file, and this will go within a ViewModel directory, and we will name this store_info.php. And this will just be a PHP file and we will use my tool Beeline to just populate a simple view model. This is just the standard signature for a view model. It's a simple class that implements argument interface. This is just a simple marker interface that tells Magento that this is a view model and this is required to inject arguments into blocks. There's not much more to it. And I'm going to populate this file just by creating a couple functions. Functions that grab the store name and the phone number. Since methods should only do one thing, it's best to place this into separate functions, and these functions simply call store manager to get the store name as well as the configuration to get the value for the phone number. There's nothing that crazy going on here, and we are just wrapping get store name within a try-catch statement because the store manager can throw a No Such Entity exception. This isn't needed, but without it, PHPStorm will flag some warnings that are just annoying. Now we need to wire up this ViewModel to the template, and we can do this with layout.xml. This is done by passing an argument into the related block class. We are targeting the homepage, and this page is served up from the CMS module. What we know about layout handles in Magento is that this layout handle is named CMS_index_index. So we can target that handle within our module. Let's go ahead and create a new file, and this will be stored at view/frontend/layout/cms/index/index.xml. And this will just be a standard Magento layout file. Within this layout file, we will create a new block for our template, and within this block, we will make a new arguments node passing in our view model as an argument. We'll just name this store_info. And since we will be passing in a class, the xsi:type will be equal to object, and then we will just go ahead and pass in our ViewModel class. When Magento processes this layout XML file, it will take this class and inject it as an argument to this block with this name store_info. That means we can access the ViewModel by this variable name right here. And while not required, it is actually good practice to go ahead and add on a suffix to these view model arguments, and I'm going to add in _view_model. This just tells the template that this is indeed a view model and makes it very explicit on what it does. And note that if you are confused about any of this, I do have a free jumpstart course available which has an entire section dedicated to views and layouts. If you're interested in that, check it out. A link is in the description below. Our next step is to create this template named store-info.php. So let's go back into our module and within the Vue frontend directory, let's go ahead and create a new file within templates and this will be named store-info.php. And I'm just going to paste in some info to this file to save us some time. At the top of my templates, I always use var annotations to reference these specific classes that the variables relate back to. This makes debugging very easy because I can hover over the methods and command-click into them to find out exactly what's going on and go right into the methods that they call. So that's not required, but it is extremely handy when it comes time to debugging variables within templates. And to get the data from our ViewModel, we'll just call blockGetData and pass in the name of the ViewModel. That name comes from what we defined in our layout XML file right here as the argument that it's injected into the block. So nothing too magical going on at this point, but now that we have access to the ViewModel, we can call the methods that we created on it, which allows us to very easily display that data. And it's good practice to always use the escaper function from the Escaper class to escape the HTML that you are displaying from the database. This adds some security to your site, such as preventing cross-site scripting attacks. Now let's go ahead and enable the module with /bin/magento module enable acme/store. And note that there is no reason to run setup-upgrade since we have no data scripts in our module yet. Now let's go back to our homepage and refresh the page. And of course we don't see any data here, so we made a mistake. Let's go check our Magento code. So Magento is notoriously very picky about naming, so we messed something up here. The first thing I'll do is check the file name conventions and the locations for the templates. But I will actually notice right away that within this body tag, we are just adding a block to the page. This doesn't actually do anything because we have nothing to render the block. Instead, we must add this block to a container that already exists on the page. Since we want it in that main area, let's go ahead and wrap this block within a call to reference container and we will reference content. And this is the main content container that is in the layout XML. So after going ahead and adding this code, let's go ahead and run /bin/magento cache flush to purge this layout XML. Now when we go back to the homepage and refresh and cross our fingers that we made the correct change, we will see our data being displayed on the page. When working with Magento, it's very easy to get frustrated on simple things like this, But just know that if something's not displaying or working properly, you just aren't following the framework conventions. So do your best to just keep calm and go back through your code and make sure everything's named properly, formatted correctly, and in the right locations. Okay, so we set up a ViewModel and we are getting data back from our config. So this pattern works for about 99% of your needs. ViewModel for data, layout XML to wire it up to a block. And a template to render it. There is absolutely no need for a block class here. Okay, now let's look at when you actually need to create and use a block class. To be upfront, custom block classes are definitely legacy. While they aren't technically obsolete, you shouldn't be constantly wanting or needing to create them. Just ask yourself, do I need access to the layout system? Because 9 times out of 10, the answer is no, and a view model will be much faster to create and much more efficient. It's actually pretty rare in modern Magento development to need to create a custom block class, as view models will handle just about everything that you need. But there's one thing that view models aren't able to do, and that is access and interact with the layout system. View models are intentionally isolated from the layout. This means that they can't do things like check to see if another block is in the layout or introspect the page or layout structure to see what is about to be rendered. But that's by design because it keeps view models fast and focused on doing just one thing. Block classes have full access to getLayout, which opens up the ability to do things like checking if another block exists, getting or finding references to other blocks, introspecting containers to see what's inside of them, or programmatically removing or modifying blocks from the layout. You'll see this pattern a lot in the Magento core, but just because the core does it doesn't mean you should. A lot of the core code has been around since before the time view models were implemented. And they just haven't been refactored yet. However, block classes are still valid for logic that needs to be aware of the layout. Let's check out an example of this right now. Say we have a content block that needs to know if a certain block exists. Maybe we want to add a CSS class to it or just the output of a block based on whether we're in a 1 or 2 column layout. A ViewModel can't answer the question, does this block exist? Because it has no access to the layout. But a block class can, and this is where it comes into play. Let's go ahead and create a new block class within our Acme Store module. Let's go ahead and create a new file at block/layout-detector.php, which will be the name of our block. And within this file, I will again go ahead and use Beeline to populate the boilerplate for this block class. Now, all block classes can extend this base template class. This gives them access to other properties and functions on that parent class, like the ability to fetch the layout. Now if we take a look at this, this looks eerily similar to our ViewModel, but that'll change once we have access to the layout. Let's go ahead and populate this class with a couple basic functions. The hasStoreInfo function will actually call the layout and check if a specific block is available, which is our store info block. This is something a view model just can't do because it doesn't have access to this getLayout function. And getContainerClass we will use in our template file, which we'll just call that hasStoreInfo function and return one of two classes. Next, we need to create a block in the layout XML which uses this block class. So let's go ahead and go back to our CMS index index XML file. And within reference container, let's go ahead and just add another block. This will have the name of layout detector, reference our new block class, and then call a new layout detector template file. What's different from a regular block reference though, is that this includes this class attribute assigned to that new block class. This class attribute is optional, and if it's missing like it is on our store info block, it actually uses the base class of template. And since our block class does contain all of the display logic, we need to render this class. We of course don't need to inject any arguments into the block as view models. Now let's go ahead and create this layout detector template. So within templates, let's go ahead and create a new file named layout-detector.php. And for now I will just paste some info in that is similar to the other template that we have. But instead of calling the view model, we will just call block get_container_class, which is referencing that layout detector block class. So again, if we annotate it here with a var annotation, we can easily command-click into the class to go directly to that block class function. Now if we go ahead and run bin/magento cache flush to flush the cache, and we will go ahead and refresh the page, we will see our new hello world div appear. And if we go ahead and inspect this element with Chrome DevTools, we will see has store info is the class that was added to that div. Now if we actually head back to the layout XML and we will go ahead and comment this block out and again flush the cache. If we'll go back to our homepage and then refresh the page, we will see that that other block did go away. But if we go ahead and inspect Hello World, it says no store info. So we know our block class is fetching the layout and determining if a specific block exists and then conditionally rendering a CSS class based on what it finds. If you need access to the layout, this is one of those 1% situations and you can absolutely just use a block class. That's what they're there for. When you're digging through the core code or in some badly coded third-party modules, you'll likely see these block classes doing things that could be be easily achievable just by using a ViewModel. For new code, the rule is simple. When you need data in a template, use a ViewModel. And if you need to interact with the layout system, just use a Bloc class. And just never place business logic in templates. They are just for the presentation of data, nothing else. And honestly, needing layout interaction is really rare. Most of the time you'll be just fetching and formatting data, and that's definitely the job of the ViewModel. And a quick note on caching. The examples I used here are cache safe because the layout structure is static. That layout block either exists in the layout XML or it doesn't, and it produces the same result for every visitor. If you had logic in place that was related to user-specific data, like does this customer have items in the cart, that would be a caching problem. Layout introspection is determined at build time, not at runtime. Well, I hope I convinced you to only use block classes when you need to, and that view models are definitely the way forward to manage all of the business logic for your presentational layer. If you want to learn all about the inner workings of Magento development like this from the ground up, check out my Magento 2 Coding Kickstart course at m.academy, which covers the view layer and all of the other core areas of the framework in much greater detail. Until next time, I'm Mark from m.academy, and I hope You enjoyed this lesson.










