Sulu 2.0-alpha1 released

After my blog post explaining our technical choices for Sulu 2.0 we have decided that it is the right time to make a first alpha release although quite some functionality is still under development. You should see it more as a developer preview in order to retrieve feedback from you. Please tell us about any bugs you might encounter and anything that is missing in our UPGRADE.md file.

The most important entities already have a list and a form, so the most basic functions can now be tested. The old administration interface is gone, so from now on the new admin is accessible on the URL "/admin" - not anymore on "/admin/v2" for those of you who have already played around with our develop branch.

There is also the ExampleEventBundle which showcases how easy it is to display a simple list with an edit form for an existing API. This bundle will also be the example for the further instructions on how to extend our administration interface.

The single steps have separate Pull Requests in the ExampleEventBundle which will be linked in the text below. They start from a point where an API is already available. The API was built with an interesting architecture using a CommandBus, but that's the topic of a separate blog post.

List your entities

In this PR a simple list for the Example entity is implemented. The crucial part here is that the API is described using our list-builder metadata. The metadata has now to be written using the XML format and replace the current FieldDescriptors (actually not really replacing, they are using them under the hood).

<property name="id" list:translation="example_event.id">
    <orm:field-name>id</orm:field-name>
    <orm:entity-name>Sulu\Bundle\ExampleEventBundle\Model\Event</orm:entity-name>
</property>

The file contains a few of the above properties, explaining the name, which translation should be used for the frontend and also from which field of which entity the data can be retrieved. This is necessary because we also build the query for the database based on this definition.

This metadata will be transformed for our generic list component, which will only retrieve the values important for it to save bandwidth. The other important thing is to create an Admin class, which was already existing in the 1.x series of Sulu, although some of the interfaces did slightly change.

<?php

// some namespace and use statements

class EventAdmin extends Admin
{
    public function getNavigation(): Navigation
    {
        $rootNavigationItem = $this->getNavigationItemRoot();

        $module = new NavigationItem('example_event.events');
        $module->setPosition(40);
        $module->setIcon('su-calendar');

        $events = new NavigationItem('example_event.events');
        $events->setPosition(10);
        $events->setMainRoute('example_event.event_datagrid');

        $module->addChild($events);
        $rootNavigationItem->addChild($module);

        return new Navigation($rootNavigationItem);
    }

    public function getRoutes(): array
    {
        return [
            (new Route('example_event.event_datagrid', '/events', 'sulu_admin.datagrid'))
                ->addOption('title', 'example_event.events')
                ->addOption('adapters', ['table'])
                ->addOption('resourceKey', 'events'),
        ];
    }
}

Lets have a look at the new getRoutes function in the Admin first. It will return all the routes for the administration interface offered by this bundle. A route receives an ID (example_event.event_datagrid), a path (/events) and a view (sulu_admin.datagrid) as mandatory arguments. The view is registered in the frontend and will be rendered in the main area of our admin if the route is requested.

The cool thing is that we have some predefined views, which can be further customized using options on the route. So our datagrid view accepts a resourceKey option that tells the datagrid which kind of entities it should load using the previously defined metadata (which also have to be registered in the sulu_admin config, the example uses a PrependExtension for that). The other options let you decide which title to show and what adapter the list uses. The adapters for the Datagrid are another interesting topic on its own because besides the few predefined ones it is easily possible to add your own ones, but that's again a topic for a separate blog post.

The NavigationItems are now simply returned by the getNavigation method instead of being created in the constructor. The reason for this is that it should give you less headache when overwriting an Admin service.

Otherwise the code is quite similar, except for the NavigationItem which has a new setMainRoute function that takes the ID of one of the previously defined routes. A click on this NavigationItem in the frontend will then redirect the user to the given route.

Make your entity editable

The second PR shows how to implement an add and edit form for the custom Event entity. There are a few more routes added to the Admin class:

<?php

(new Route('example_event.event_add_form', '/events/add', 'sulu_admin.resource_tabs'))
    ->addOption('resourceKey', 'events'),
(new Route('example_event.event_add_form.detail', '/details', 'sulu_admin.form'))
    ->addOption('tabTitle', 'example_event.details')
    ->addOption('backRoute', 'example_event.event_datagrid')
    ->addOption('editRoute', 'example_event.event_edit_form.detail')
    ->setParent('example_event.event_add_form'),

(new Route('example_event.event_edit_form', '/events/:id', 'sulu_admin.resource_tabs'))
     ->addOption('resourceKey', 'events'),
(new Route('example_event.event_edit_form.detail', '/details', 'sulu_admin.form'))
    ->addOption('tabTitle', 'example_event.details')
    ->addOption('backRoute', 'example_event.event_datagrid')
    ->setParent('example_event.event_edit_form'),

There are two separte forms for adding and editing registered, because they should also have a separate route. Each form is split into two different kind of views: The ResourceTabs already receives a resourceKey and loads the entity using this key and the ID from the route. We have decided to implement it this way to avoid reloading the same entity on every single tab.

Each tab in the form (in the above example there is only one per form) is a form view and will receive the loaded entity from the ResourceTabs view. The form view then defines the title of the tab it is shown in and the backRoute defining to which route a click on the back button redirects.

Then the form will display the loaded entity using some metadata. We have decided to use the properties part of the template XML files, so we extracted that and they can be put into the Resources/config/forms directory (or whatever you have defined in the mentioned PrependExtension).

<?xml version="1.0" ?>
<properties xmlns="http://schemas.sulu.io/template/template"
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xsi:schemaLocation="http://schemas.sulu.io/template/template http://schemas.sulu.io/template/properties-1.0.xsd">
    <property name="title" type="text_line">
        <meta>
            <title lang="de">Titel</title>
            <title lang="en">Title</title>
        </meta>
    </property>
    <property name="description" type="text_area">
        <meta>
            <title lang="de">Beschreibung</title>
            <title lang="en">Description</title>
        </meta>
    </property>
    <property name="startDate" type="date">
        <meta>
            <title lang="de">Start</title>
            <title lang="en">Start</title>
        </meta>
    </property>
    <property name="endDate" type="date">
        <meta>
            <title lang="de">Ende</title>
            <title lang="en">End</title>
        </meta>
    </property>
</properties>

This XML should look fairly familiar to Sulu users. The properties only define a name, type and a title to display this information in a form. All the other tags from the template XML also work in here since the form for content and for any other entity now uses the same mechanism. That means no more templates to render on the server have to be created for datagrids or forms. 

Offer a field type to assign your entity

Since it doesn't make a lot of sense to only have a list and a form on its own we have decided to make it easily possible to create a new field type for forms to assign the entities. This is what's being used in the last PR of this blog post.

And again there is only some configuration to add to retrieve this functionality.

<?php

$container->prependExtensionConfig(
    'sulu_admin',
    [
        'field_type_options' => [
            'selection' => [
                'event_selection' => [
                    'adapter' => 'table',
                    'displayProperties' => ['title', 'startDate', 'endDate'],
                    'icon' => 'su-plus',
                    'label' => 'example_event.event',
                    'resourceKey' => 'events',
                    'overlayTitle' => 'example_event.events',
                ],
            ],
            'single_selection' => [
                'single_event_selection' => [
                    'auto_complete' => [
                        'displayProperty' => 'title',
                        'searchProperties' => ['title'],
                        'resourceKey' => 'events',
                    ],
                ],
            ],
        ],
        // more configuration
    ]
);

Using the prependExtension every bundle can hook some more configuration into the sulu_admin configuration. Here you have to pass some more fieldTypeOptions. There are some "abstract" field types that can be customized when useing with your entity. In this example we say that we'd like to have a selection for our event, the selection being the "abstract" field type and event_selection the name of the registered field type. The array that is indexed by this key contains some properties which will be passed to the "abstract" field type.

E.g. the auto_complete version of the event_selection only needs to know which entity to load, which of its property should be used to display in the result set and after which properties should be searched.

The event_selection field type is a little bit more complicated. It has similar properties but in addition it also needs to know which datagrid adapter to use since it shows a datagrid internally. It also allows to customize some texts and icons as well.

Conclusion

This was a short introduction to show how much you can achieve without writing a single line of JavaScript. We hope that you like this new way of extending Sulu and would like to hear your feedback. So don't hesitate to contact us on Twitter or on our Slack channel (invites can be requested using the form on our website).