Skip to content

Add new content type

Intro

This section talks about how to register new content types. Once a content type is registered, you can use content models of that type throughout the wf-html.

Create the entity

Create a custom entity in the project.

In v4 add @JMSS\Groups({"list"}) to all fields that will be used in the editor.

In v5+ JMS serializer was replaced by Symfony Serializer, so the annotation should be @Serializer\Groups({"list"}). The annotation must be imported with use Symfony\Component\Serializer\Annotation as Serializer;

Register entity

Register entity parameter/repository service in DependencyInjection\CmsExtension. These are required by the CMS to link entities in your database to the editor

php
public function load(array $configs, ContainerBuilder $container)
{
    // ...

    $this->handleEntityConfig($container);
}


protected function handleEntityConfig(ContainerBuilder $container)
{
    foreach (["restaurant" => Restaurant::class] as $key => $class) {
        $container->setParameter(sprintf('wf_cms.entity.%s.class', $key), $class);

        $repositoryService = new Definition($class);
        $repositoryService->setFactoryService('doctrine.orm.default_entity_manager');
        $repositoryService->setFactoryMethod('getRepository');
        $repositoryService->setArguments(array($class));

        $repositoryId = sprintf('wf_cms.repository.%s', $key);
        $container->setDefinition($repositoryId, $repositoryService);
    }
}

Index entity in ElasticSearch

For this edit the configuration file app/config/misc/fos_elastica.yml and add a new type.

yaml
indexes:
    %fos_index_name%:
         client: default
         finder: ~
         settings:
             index:
                 analysis:
                     analyzer:
                         lc-af:
                             tokenizer: standard
                             filter: [ standard, lowercase, asciifolding ]
         types:
             restaurant:
                 _all:
                     analyzer: lc-af
                 properties:
                     title: { boost: 10 }
                     description: ~
                     foodType: { boost: 8 }
                     restaurantStatus:
                         type: keyword
                         property_path: getStatus
                     createdAt:
                         type: date
                     updatedAt:
                         type: date
                 persistence:
                     driver: orm
                     model: App\Bundle\CmsBundle\Entity\Restaurant
                     provider: ~
                     listener: ~
                     finder: ~

Register in the allowed content types parameter

Add the new type to allowed types parameter by creating a compiler pass:

php
$baseTypes = $container->getParameter('wf_cms_admin.content_type.allowed');
$allTypes = array_merge($baseTypes, ['restaurant']);

$container->setParameter('wf_cms_admin.content_type.allowed', $allTypes);

Create an ElasticSearch finder service for the new type

xml
 <service id="wf_cms.search.restaurant_finder" parent="wf_cms.search.general_finder" class="App\Bundle\CmsBundle\Search\RestaurantFinder"></service>

Configure the ElasticSearch finder service

Add the new type to DependencyInjection/AppCmsAdminExtension.php so that ElasticSearch uses the new type to filter the results.

php
<?php
public function load(array $configs, ContainerBuilder $container)
{
    // ...

    $this->handleSearchConfig($container);
}


protected function handleSearchConfig(ContainerBuilder $container)
{
    $indexName = $container->getParameter('fos_index_name');

    $findables = ['restaurant'];
    foreach($findables as $contentType) {
        $finderDefinition = $container->getDefinition(sprintf('wf_cms.search.%s_finder', $contentType));
        $finderDefinition->replaceArgument(0, new Reference(sprintf('fos_elastica.index.%s.%s', $indexName, $contentType)));
        $finderDefinition->replaceArgument(1, new Reference(sprintf('fos_elastica.elastica_to_model_transformer.%s.%s', $indexName, $contentType)));
    }

}

Create the PHP Finder class

php
<?php
// in src/App/Bundle/CmsBundle/Search/Finder/RestaurantFinder.php

class RestaurantFinder extends BaseFinder
{
    protected function getBaseQB()
    {
        $qb = parent::getBaseQB();

        $qb->sortBy(['updatedAt' => 'desc']);

        $statusQuery = new Term([
            'restaurantStatus' => Restaurant::STATUS_ENABLED
        ]);
        $qb->addQuery($statusQuery);

        return $qb;
    }

}

Create the JavaScript content type

javascript
// Bundle/CmsAdminBundle/Resources/public/javascripts/content_type/restaurant_content_type.js
define([
    'cnf',
    'admin/model/content/restaurant_content_model',
    'wfed/collection/sortable_collection',
    'wfed/content_type/base_content_type'
], function (
    cnf,
    RestaurantContentModel,
    RestaurantCollection,
    Base
) {
    return Base.extend({
        wfId: 'content_type/restaurant_content_type',
        typeId: 'restaurant',
        modelClass: RestaurantContentModel,
        indexName: 'restaurant',

        getContentModel: function (moduleModel) {
            return new this.modelClass;
        },

        getCollection: function () {
            var collection = new RestaurantCollection();
            collection.model = RestaurantContentModel;

            return collection;
        },

        getAddViewPath: function () {
            return null;
        },

    });
});
javascript
// Bundle/CmsAdminBundle/Resources/public/javascripts/model/content/restaurant_content_model.js
define(['backbone',
    'wfed/model/content/base_content_model'
], function(Backbone,
            Base
){
    return Base.extend({
        wfId: 'wfed/model/content/restaurant',
        defaults: {
            id: null, //the DB id
        },
        contentType: 'content_type/restaurant_content_type',

        urlRoot: '/restaurant/',
    });
});

Register the new JavaScript custom type

Overwrite the factory and factory loader files to add the new type:

javascript
 // Bundle/CmsAdminBundle/Resources/public/javascripts/content_type_factory.js
define(['_wfed/content_type_factory'], function (baseContentFactory) {
    const baseGetSearchableTypesIds = baseContentFactory.getSearchableTypesIds;
    baseContentFactory.getSearchableTypesIds = function () {
        return baseGetSearchableTypesIds().concat(['restaurant']);
    };

    const baseGetCollectionTypesOrder = baseContentFactory.getCollectionTypesOrder;
    baseContentFactory.getCollectionTypesOrder = function () {
        return baseGetCollectionTypesOrder().concat(['restaurant']);
    };

    return baseContentFactory;
});
javascript
// Bundle/CmsAdminBundle/Resources/public/javascripts/content_type_factory_loader.js
define([
    'wfed/content_type_factory',
    'cnf'], function (contentTypeFactory,
                      cnf) {
    var req = require.context('wfed/content_type', false, /_content_type.js$/);
    _.each(cnf.contentTypes, function (type) {
        var key = contentTypeFactory.getNormalizedContentTypeId(type),
            location = './' + type + '_content_type.js';

        if (key === 'restaurant') {
            this.types[key] = require('admin/content_type/restaurant_content_type');
        } else {
            this.types[key] = req(location);
        }
    }, contentTypeFactory);
});
javascript
// ...

adminConfig.webpack = adminConfig.webpack || {};
adminConfig.webpack.resolve = adminConfig.webpack.resolve || {};
adminConfig.webpack.resolve.alias = adminConfig.webpack.resolve.alias || {};
// console.log(Object.keys(adminConfig));
Object.assign(adminConfig.webpack.resolve.alias, {
    'wfed/content_type_factory': 'admin/content_type_factory',
    'wfed/content_type_factory_loader': 'admin/content_type_factory_loader',
});

Change the search result twig template

Overwrite the template of the new type displayed in the search popup window:

yaml
// app/admin/config/routing.yml
wf_cmseditor_javascripttemplate_renderitem:
    path: /bundles/wfcmseditor/javascripts/template/render_item.html
    defaults:
        _controller: 'AppCmsAdminBundle:EditorJavascriptTemplate:renderItem'
php
<?php

namespace App\Bundle\CmsAdminBundle\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;

class EditorJavascriptTemplateController extends \Wf\Bundle\CmsEditorBundle\Controller\JavascriptTemplateController
{
    /**
     * @Template()
     * @return array
     */
    public function renderItemAction()
    {
        return parent::renderItemAction();
    }

}
html
// Bundle/CmsAdminBundle/Resources/views/EditorJavascriptTemplate/renderItem.html.twig
{% extends "@WfCmsEditor/JavascriptTemplate/renderItem.html.twig" %}

{% block item_info %}
    {{ parent() }}

    <% if (item.food_type) { %>
    <div class="as-field author">
        <span class="label">{{ 'adv_search.fields.foodType'|trans }}</span>
        <span class="value"><%= item.food_type %></span>
    </div>
    <% } %>
{% endblock %}

Add JavaScript translations

Add the translation for the edit popover:

html
// Bundle/CmsAdminBundle/Resources/views/Javascript/i18n.html.twig

{% extends "WfCmsBaseAdminBundle:Javascript:base_i18n.js.twig" %}

{% block custom_module_editor_dialog %}
    {# ... #}
    restaurant: '{{ 'module_edit_dialog.restaurant'|trans }}',
    {# ... #}
{% endblock %}

Use the content type

To use it:

v4-6
html
<div wf-module="wfed/composite/module"
    wf-role="restaurant">
    <span data-bind="text:restaurant_title" />
</div>
v7+
html
<div wf-module="wfed/composite/module"
    wf-role="restaurant">
    <span data-bind="text:restaurant.title" />
</div>

Note: The way of accessing properties has changed in v7+, in previous versions it was contentType_propertyName (_ separator), in v7+ it's contentType.propertyName (standard JavaScript syntax, . separator)