Service Contracts are here again for this task. Creating CMS blocks and pages is a breathe... with a point of attention about stores relation that is discussed in this post.

For the sake of being more complete, we will integrate the creation of a CMS block and a CMS page in an install script. This is because we often need to use those installers to make sure our CMS contents are properly created on each Magento instance using our code.

Want the whole code?

Check this out!

Module creation

The example module used here is Herve_CmsInstall.

You may need to create a module if you wish to use an installer to create your CMS block and/or page.

This is out of the scope of the module, but here is how Herve_CmsInstall structure looks like:

Basic module structure

Installer creation

In Magento 2, the data and structure installation and upgrades have to be in the Setup folder of your module. Moreover, depending on the type of installer you are doing, the name of the file defers. In our case, we are creating some CMS items so we are basically adding data to the database. So our installer class name must be InstallData which implements the \Magento\Framework\Setup\InstallDataInterface. And, as required by the interface, it must have an install method.

The base of our installer looks like this:

// Herve/CmsInstall/Setup/InstallData.php
namespace Herve\CmsInstall\Setup;

use Magento\Framework\Setup\InstallDataInterface;  
use Magento\Framework\Setup\ModuleContextInterface;  
use Magento\Framework\Setup\ModuleDataSetupInterface;

class InstallData implements InstallDataInterface  
{
    /**
     * Installs data for a module
     *
     * @param ModuleDataSetupInterface $setup
     * @param ModuleContextInterface $context
     * @return void
     */
    public function install(ModuleDataSetupInterface $setup, ModuleContextInterface $context)
    {
        // TODO: Implement install() method.
    }
}

Creating a CMS block

By looking at the Service Contracts shipped with Magento, we can see that we have the proper API to create a CMS block: \Magento\Cms\Api\BlockRepositoryInterface.

This interface exposes the save method which allows us to (you guessed it) save a CMS block. And this save method takes a \Magento\Cms\Api\Data\BlockInterface as parameter.

So, basically, what we have to do is :

  • instanciate a new BlockInterface which is our empty CMS block

  • set the required data on this BlockInterface

  • Pass this BlockInterface to the save method of the BlockRepositoryInterface

So, at the end of the day, our class may look like this:

// Herve/CmsInstall/Setup/InstallData.php
namespace Herve\CmsInstall\Setup;


use Magento\Cms\Api\BlockRepositoryInterface;  
use Magento\Cms\Api\Data\BlockInterface;  
use Magento\Cms\Api\Data\BlockInterfaceFactory;  
use Magento\Framework\Setup\InstallDataInterface;  
use Magento\Framework\Setup\ModuleContextInterface;  
use Magento\Framework\Setup\ModuleDataSetupInterface;

class InstallData implements InstallDataInterface  
{
    /**
     * @var BlockRepositoryInterface
     */
    private $blockRepository;
    /**
     * @var BlockInterfaceFactory
     */
    private $blockInterfaceFactory;

    public function __construct(
        BlockRepositoryInterface $blockRepository,
        BlockInterfaceFactory $blockInterfaceFactory
    ) {
        $this->blockRepository = $blockRepository;

        // Here we need to use a factory for the \Magento\Cms\Api\Data\BlockInterface
        // This is because we will need to create a new instance of a CMS block rather than
        // being "stuck" in a Singleton.
        $this->blockInterfaceFactory = $blockInterfaceFactory;
    }

    /**
     * Installs data for a module
     *
     * @param ModuleDataSetupInterface $setup
     * @param ModuleContextInterface $context
     * @return void
     */
    public function install(ModuleDataSetupInterface $setup, ModuleContextInterface $context)
    {
        $this->createCmsBlock();
    }

    /**
     * Create a CMS block
     */
    public function createCmsBlock()
    {
        /** @var BlockInterface $cmsBlock */

        // And there we create a new instance of the \Magento\Cms\Api\Data\BlockInterface
        // by using the create() method of the factory that Magento
        // automatically generates for us at runtime.
        // @see http://devdocs.magento.com/guides/v2.0/extension-dev-guide/code-generation.html
        $cmsBlock = $this->blockInterfaceFactory->create();

        // We can then use the exposed methods from
        // \Magento\Cms\Api\Data\BlockInterface to set data to our CMS block
        $cmsBlock->setIdentifier('block-identifier')
            ->setTitle('Block Title')
            ->setContent('Block Content')

            // WT* is this setData? It is not exposed in the interface so why is it here?
            // Please continue reading for explanations!
            ->setData('stores', [0]);

        // And use the \Magento\Cms\Api\BlockRepositoryInterface::save
        // to actually save our CMS block
        $this->blockRepository->save($cmsBlock);
    }
}

Creating a CMS page

I won't go again thru the steps of things to do but rather just paste the code of the complete class. There you will find and understand what is required to create a CMS page.

Just notice that the ->setData('stores', [0]); is also there for the CMS page...

// Herve/CmsInstall/Setup/InstallData.php
namespace Herve\CmsInstall\Setup;


use Magento\Cms\Api\BlockRepositoryInterface;  
use Magento\Cms\Api\Data\BlockInterface;  
use Magento\Cms\Api\Data\BlockInterfaceFactory;  
use Magento\Cms\Api\Data\PageInterface;  
use Magento\Cms\Api\Data\PageInterfaceFactory;  
use Magento\Cms\Api\PageRepositoryInterface;  
use Magento\Framework\Setup\InstallDataInterface;  
use Magento\Framework\Setup\ModuleContextInterface;  
use Magento\Framework\Setup\ModuleDataSetupInterface;

class InstallData implements InstallDataInterface  
{
    /**
     * @var BlockRepositoryInterface
     */
    private $blockRepository;
    /**
     * @var BlockInterfaceFactory
     */
    private $blockInterfaceFactory;
    /**
     * @var PageRepositoryInterface
     */
    private $pageRepository;
    /**
     * @var PageInterfaceFactory
     */
    private $pageInterfaceFactory;

    public function __construct(
        BlockRepositoryInterface $blockRepository,
        BlockInterfaceFactory $blockInterfaceFactory,
        PageRepositoryInterface $pageRepository,
        PageInterfaceFactory $pageInterfaceFactory
    ) {
        $this->blockRepository = $blockRepository;
        $this->blockInterfaceFactory = $blockInterfaceFactory;
        $this->pageRepository = $pageRepository;
        $this->pageInterfaceFactory = $pageInterfaceFactory;
    }

    /**
     * Installs data for a module
     *
     * @param ModuleDataSetupInterface $setup
     * @param ModuleContextInterface $context
     * @return void
     */
    public function install(ModuleDataSetupInterface $setup, ModuleContextInterface $context)
    {
        $this->createCmsBlock();
        $this->createCmsPage();
    }

    /**
     * Create a CMS block
     */
    public function createCmsBlock()
    {
        /** @var BlockInterface $cmsBlock */
        $cmsBlock = $this->blockInterfaceFactory->create();
        $cmsBlock->setIdentifier('block-identifier')
            ->setTitle('Block Title')
            ->setContent('Block Content')
            ->setData('stores', [0]);

        $this->blockRepository->save($cmsBlock);
    }

    /**
     * Create a CMS page
     */
    private function createCmsPage()
    {
        /** @var PageInterface $cmsPage */
        $cmsPage = $this->pageInterfaceFactory->create();
        $cmsPage->setIdentifier('page-identifier')
            ->setTitle('Page Title')
            ->setContentHeading('Content Heading')
            ->setContent('Page HTML content')
            ->setPageLayout('1column')
            ->setData('stores', [0]);

        $this->pageRepository->save($cmsPage);
    }
}

So, WT* are those setData()?

When creating CMS blocks and pages, we may need to specify on which stores they are available. And the Magento 2 CMS API is not very clever on that part...

As you may have noticed, both the block and page repository interfaces do not expose any method allowing us to chose on which store the content must be available.

So the first option would be to let Magento do its own job and let it chose for us. But Magento does not do a wise choice: it will make our CMS block/page available only for the default store view of our shop!

Why is that?

(Below is a demonstration for a CMS block also working for a CMS page)

  • The interface responsible for saving the CMS block is \Magento\Cms\Api\BlockRepositoryInterface

  • It's implementation is \Magento\Cms\Model\BlockRepository::save in which we can see:

$storeId = $this->storeManager->getStore()->getId();
$block->setStoreId($storeId);
  • And if you dig a little, you'll find that $storeId is the default store ID. So, our CMS block instance has its store_id data which value is the default store ID.

  • Then, when actually saving the CMS block, the \Magento\Cms\Model\ResourceModel\Block\Relation\Store\SaveHandler::execute method is called. How it is called is still a bit of a mystery... but it really is called and its job is to link a CMS block to the store views it is available on.

  • And this method has this line: $newStores = (array)$entity->getStores(); where, of course, $entity is the implementation of the \Magento\Cms\Api\Data\BlockInterface. This implementation is the good old \Magento\Cms\Model\Block which actually has the getStores method.

  • And, guess what, the \Magento\Cms\Model\Block::getStores method returns the store_id data that has been set earlier.

This explains why Magento automatically affects a CMS block to the default store view if there is no other info that is set on the BlockInterface.

So the question is: how do we chose what store views our CMS content is attached to? How to make it available for all stores views (aka store ID 0)?

We have to do it the dirty way by using setData('stores', [ARRAY_OF_STORE_IDS]).

As Magento API interfaces do not expose a way allowing us to chose the store views, we have to use methods available on their implementations... Which is opened to breaking changes as soon as the implementations of the interfaces are not able to understand the setData method.

Here is how I came up with using the setData method:

  • Is there a way to set store views by using the interface? No!

  • So, let's find out which concrete class implements the interface. There we go: it's \Magento\Cms\Model\Block

  • Does this class have some king of method like setStores or setStoreIds that makes the job? No!

  • So let's climb up the inheritance and...

  • ... there we are in \Magento\Framework\DataObject (which basically is the M2 version of the M1 Varien_Object).

  • This leaves me with 2 options:

    1. Use the good ol' magic setter: setStores() which may lead to confusion to my fellow developers who may think that this method is exposed in the interface
    2. Be more explicit and tell "hey I'm using a method that is not in the interface".

I went for option 2.