Creating AFX-based Applications / Backend Modules

Using Atomic Fusion in Flow Applications and Backend Modules

As Neos is built upon the Flow framework, you can write PHP applications based on the MVC architecture alongside your Neos instance. To do this, there are generally three architecture options:

Each of these options have the same core principles, i.e. your application is structured as MVC application. This means all knowledge on how you write plugins can equally be applied to writing custom applications or backend modules.

However, these different options vary a bit in the way they are embedded in Neos, which means there are some differences in the View layer:

  • In custom applications, the application is fully responsible for rendering all output, so for example it needs to render <html> and <head>-Tags as well.
  • In custom backend modules, you only render the main part of the <body>, the wrapping is provided by the Neos backend module APIs (as explained in the Custom Backend Modules chapter).
  • In custom plugins, the plugins are embedded as part of the frontend website output (as a content element). Thus, you only render a HTML snippet. This is explained in Creating a Plugin.

#Using Atomic Fusion to structure the View

We'll now zoom in specifically into the View layer of our custom applications. Historically, the View has been mostly implemented with Fluid, which is a classical templating engine similar to Smarty or Twig. 

As we've made great experiences in using AFX and Atomic Fusion for the frontend output, we now recommend to use the same patterns for custom applications as well. This allows you to compose the view by putting different components together, instead of having one huge template per-action.

#Set Up

The following steps need to be taken to set up AFX.

#1. require AFX

Through composer, you need to add neos/fusion and neos/fusion-afx as dependencies.

#2. In your controller, use FusionView for rendering

set the $defaultViewObjectName to the FusionView for rendering.

YourController.php
use Neos\Flow\Mvc\Controller\ActionController;
use Neos\Fusion\View\FusionView;

class YourController extends ActionController
{
    protected $defaultViewObjectName = FusionView::class;

    // ...
}

pick FusionView of Neos.Fusion

If being in a Neos based projects, one FusionView resides in the Neos.Fusion package, and the other one in Neos.Neos.  The FusionView inside Neos.Neos is not meant to be used outside of the Neos core.

Make sure to choose the FusionView inside Neos.Fusion!

#Suggestions for Fusion structure

Following and adapting the Neos best practices, we suggest to use the following file structure:

directory
Resources/Private/Fusion
├── Root.fusion
│
├── Integration
│   └── Controller
│       ├── [ControllerName]
│       │   └── [ControllerName][ActionName].fusion
│       └── ...
│
└── Presentation
    ├── Atom
    │   ├── ...
    │   └── ...
    │
    ├── Molecule
    │   └── ...
    │
    ├── Page
    │   ├── AbstractPage.fusion
    │   └── DefaultPage.fusion
    │
    └──BodyLayout
        └── Default.fusion
 
#Root.fusion

In your package, add a Root.fusion with the following contents:

Resources/Private/Fusion/Root.fusion
# you need to include ALL packages you want to use here, there is no AutoInclude!
include: resource://Neos.Fusion/Private/Fusion/Root.fusion

# include all files in here
include: **/*.fusion

As there is no AutoInclude of Fusion files through Settings like in Neos currently, you need to include the Fusion for all packages you want to activate manually.

#Integration/Controller/[Controller+Action].fusion

For each controller action of your package, add a corresponding file with  contents like this:

Resources/Private/Fusion/Integration/Controller/BlogPost/BlogPostIndex.fusion
Your.Package.BlogPostController.index = Your.Package:Page.DefaultPage {
    body.content = afx`
        this should be the main body content.
    `
}

FusionView always maps your current controller/action to a path, which is then used for rendering.

We recommend to create a DefaultPage which contains the normal rendering (see below).

#Presentation/Atom/...
Presentation/Molecule/...

For bigger projects, it might be helpful to structure your components according to atomic design into Atoms, Molecules and Organisms.

#Presentation/Page/AbstractPage.fusion

Neos ships a default Neos.Neos:Page Fusion object, which is however not usable for standalone applications. The following Fusion object contains a stripped-down version of this, ready for inclusion into your project.

Currently, we recommend that you copy this file into your project, as the standard might still evolve further.

Resources/Private/Fusion/Presentation/Page/AbstractPage.fusion
# This is a trimmed-down version of Neos.Neos:Page, usable for standalone usage in Flow applications.
#
# The "public API", like "head.titleTag.content", "body", or "body.javascripts" is exactly the same as
# in Neos.Neos:Page.
prototype(Your.Package:Page.AbstractPage) < prototype(Neos.Fusion:Component) {
    # add the DocType as processor; because it will break AFX.
    @process.addDocType = ${'<!DOCTYPE html>' + value}

    head = Neos.Fusion:Join {
        titleTag = Neos.Fusion:Tag {
            tagName = 'title'
        }
        # Script and CSS includes in the head should go here
        stylesheets = Neos.Fusion:Join
        javascripts = Neos.Fusion:Join
    }

    body {
        # Script includes before the closing body tag should go here
        javascripts = Neos.Fusion:Join
        # This processor appends the rendered javascripts Array to the rendered template
        @process.appendJavaScripts = ${value + this.javascripts}
    }

    renderer = afx`
        <html>
        <head>
            <meta charset="UTF-8" />
            {props.head}
        </head>
        <body>
            {props.body}
        </body>
        </html>
    `

    # enable Neos.Fusion:Debug helper
    @process.debugDump = Neos.Fusion:DebugDump
}

The Public API like head.titleTag.content or body or body.javascripts still works as in Neos.Neos:Page, so that means you can also load Neos packages which hook into these places and expect foreign Neos packages to work properly.

#Presentation/Page/DefaultPage.fusion

This is the main page template for your project.

Adjust it as needed, and you can also create new variants if you need them.

Resources/Private/Fusion/Presentation/Page/DefaultPage.fusion
prototype(Your.Package:Page.DefaultPage) < prototype(Your.Package:Page.AbstractPage) {
    head {
        headTags = Neos.Fusion:Component {
            renderer = afx`
                <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"/>
                <meta http-equiv="x-ua-compatible" content="ie=edge"/>
            `
        }
        stylesheets {
            app = afx`
                <link rel="stylesheet" type="text/css" href="..."/>
            `
        }

    }
    body = Your.Package:BodyLayout.Default {
        javascripts.app = afx`
            <script src="..."></script>
        `
    }
}

This prototype contains the application-specific page template. It connects JS and CSS with the BodyLayout.

We delegate rendering of the body HTML to the BodyLayout.Default prototype (shown below), as this is a Fusion Component which is fully testable (e.g. using Monocle).

#Presentation/BodyLayout/Default.fusion

We recommend to separate the body layout away from the DefaultPage, as we can then use a Fusion Component for this. With this separation, the body layout becomes renderable standalone, and can also be rendered in a style guide.

Resources/Private/Fusion/Presentation/BodyLayout/Default.fusion
prototype(Your.Package:BodyLayout.Default) < prototype(Neos.Fusion:Component) {
    content = ''
    teaserTitle = ''
    teaserText = ''

    renderer = afx`
        <main>
            <h1>{props.teaserTitle}</h1>
            <p>{props.teaserText}</p>
            {props.content}
        </main>
    `
}

#Summary: The Big Picture

How do all the different Fusion files fit together? The following sketch sums this up:

Summary:

  • Page.AbstractPage is a lighweight, api-compatible version of Neos.Neos:Page
  • Page.DefaultPage is the prototype for a full page. This is instanciated for each Controller action, with placeholders like body.content filled individually.
  • BodyLayout.Default renders the general layout of the page body, implemented as Neos.Fusion:Component. Thus, it is possible to use Monocle as style guide for the page layout as well.