I'm not sure there's a cut-and-dry "best" override method for a Controller - just as there are reasons why you might use a preference or plugin for any other class in Magento 2.
The benefit of using a plugin for the execute function is that you can make small changes to the response/layout while reducing the likelihood of conflicts with third-party plugins etc. I would expressly suggest never using an around plugin which completely bypasses the $proceed callable because this will trash any other plugins that were not yet executed. It's better to create a preference in this case.
Ultimately, I would say that using a plugin is the best method to inject your own logic if it is feasible to do so. (It's a public function, you don't need to access protected/private properties on the original class or recreate a ton of protected/private functions, and you are okay with still executing the code of the original function - or modifying the parameters/response is all that's necessary.) Done properly, plugins should be reasonably compatible across multiple modules. When you resort to a preference (or before route rewrite), then you open the door to other modules trying to claim the same class which will require additional technical work for the end-user to make them interoperable. (The plague of Magento 1 modules.)
The other answer details the plugin method, so below I will define the other two.
Creating a Preference
One is to create a direct preference for a given Controller class.
In a module <module_dir>/etc/frontend/di.xml
file:
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<preference for="Magento\Cms\Controller\Index\Index" type="Example\Module\Rewrite\Controller\Cms\Index\Index"/>
</config>
Thus the class \Example\Module\Rewrite\Controller\Cms\Index\Index
at <module_dir>/Rewrite/Controller/Cms/Index/Index.php
will be called instead of the original class. This is a 'complete rewrite' in a sense. You will only call the code from the original controller class if you extend it in the new class. I believe Magento 2 is smart enough to see this type of rewrite and transition existing plugins to use the new class in lieu of the old one.
This type of rewrite is best for a surgical change to a single controller class where a plugin won't work. (See above.) You can replicate this by using an around plugin without calling the $proceed function - however this method will skip any plugins that are lower in the sortOrder.
It is theoretically feasible to do whatever you want to do with a plugin on a public function, but keep in mind that any non-public functions the original method used will have to be recreated if necessary and non-public properties without any getters/setters will be totally inaccessible.
Before/After Routers
Finally, there are route before/after rewrites. These are the most useful if you intend to 'claim' or add additional actions to an existing route in bulk.
In the <module_dir>/etc/frontend/routes.xml
file:
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd">
<router id="standard">
<route id="uniqueidhere">
<module name="Example_Module" before="Magento_Cms"/>
</route>
</router>
</config>
The above configuration will 'claim' the original frontName defined within the Magento_Cms
module. Similar to the old Magento 1 config.xml before/after controller methods, this will cause the module to be inserted before or after the original module when Magento 2 tries to load an action for the request path.
When before
is used, you can create your own <module_dir>/Controller/<ControllerRoute>/<ActionRoute>.php
class files. If those files match an existing route, they will claim that route and be executed before the original classes. (You can always extend the originals.) Any unused actions will fall back to the original files. Alternatively, if after
is used then any new controller/action classes you create will create new available routes which can be executed under the original frontName but the existing action classes will still be used.
I haven't used this method much, so I'm not sure if plugins will respect controller overrides created this way. You can potentially add di.xml
preference
entries for these classes to instruct Magento to reroute existing plugins to the new class.