Skip to content

Resource bundle

Introduction

The resource bundle is a very important bundle in the enhavo eco system. It's responsible to register resources and defining grids and input masks and provides a handful of useful functions and workflows, helping you to handle your business model and data.

The resource bundle is completely headless and don't serve any assets. It's build to serve CRUD features and keep all customizable. If you have any requirements which the bundle does not provide, we have a good Api to easily add your own features.

This bundle is heavily inspired by the sylius resource bundle, but uses the typical enhavo type pattern

Installation

bash
$ composer require enhavo/resource-bundle

Resource

Resources are commonly standalone entities or sometimes called root entities. We use alias as abstraction for a resource, that helps us to later change the concrete model.

Register

Simple we add a book resource, where model is mandatory and factory and repository is optional.

yaml
# config/resources/book.yaml
enhavo_resource:
    resources:
        app.book: # resource name
            classes:
                model: App\Entity\Book
                factory: App\Factory\BookFactory
                repository: App\Repository\BookRepository
            label: Book
            translation_domain: ~
            priority: 10

This will create us a factory service with name app.book.factory and a repository service app.book.repository, that can be injected to any other service if necessary. If you don't define a factory or repository, a default ones with these classes Enhavo\Bundle\ResourceBundle\Factory\Factory and Enhavo\Bundle\ResourceBundle\Repository\EntityRepository will be created.

WARNING

A model can applied only once to a resource name and the repository class will always overwrite the repository class in your doctrine meta configuration!

If more configurations for the same resources name are defined, the one with higher priority will overwrite lower ones.

Give the resource name also a label, to display the name wherever it will be needed.

ResourceManager

The ResourceManager keep all factory and repository services. So you can retrieve the services with the alias from it. The factory is used to create a new entity while the repository to find an existing one. Use the save method to save the entity.

php
namespace App\Endpoint;

use Enhavo\Bundle\ApiBundle\Endpoint\AbstractEndpointType;
use Enhavo\Bundle\ResourceBundle\Resource\ResourceManager;

class BookEndpoint extends AbstractEndpointType
{
    public function __construct(
        private ResourceManager $resourceManager,
    ) {}

    public function handleRequest($options, Request $request, Data $data, Context $context)
    {
        // repository
        $repository = $this->resourceManager('app.book')->getRepository();
        $book = $repository->find($request->get('id'));
        
        // or factory
        $factory = $this->resourceManager('app.book')->getFactory();
        $book = $factory->createNew();
        
        // update entity and save
        $book->setTitle($request->get('title'));
        $this->resourceManager->save($book);
        
        // or duplicate
        $otherBook = $this->resourceManager->duplicate($book);
        
        // or validate
        $otherBook = $this->resourceManager->validate($book);
        
        // or apply transition
        $otherBook = $this->resourceManager->applyTransition($book, 'publish', 'book_graph');
        
        // or delete
        $this->resourceManager->delete($book);
        
        // or get metadata
        $metadata = $this->resourceManager->getMetadata($book);
        $label = $metadata->getLabel()

    }
}

As you can see in the above example, we don't need the doctrine entity manager here. This is encapsulated in the ResourceManager.

Hooks

The save method of the ResourceManager also fires some hooks we can catch with an event listener or subscriber.

php
class BookSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents()
    {
        return [
            ResourceEvents::PRE_CREATE => 'hook',
            ResourceEvents::POST_CREATE => 'hook',
            ResourceEvents::PRE_UPDATE => 'hook',
            ResourceEvents::POST_UPDATE => 'hook',
            ResourceEvents::PRE_DELETE => 'hook',
            ResourceEvents::POST_DELETE => 'hook',
        ];
    }

    public function hook(ResourceEvent $event)
    {
        $resource = $event->getSubject();
        
        // do something here
    }
}

Duplicate

With duplicate, you can create deep clones of a resource. To know which properties need to be copied, you have to mark up them. This is normally done by the Attribute Duplicate but you can also use configuration.

php
<?php
namspace App\Entity;

use Enhavo\Bundle\ResourceBundle\Attribute\Duplicate;
use Doctrine\Common\Collections\Collection;

class Book
{
    #[Duplicate('string', ['postfix' => ' Copy!!', 'group' => ['duplicate']])]
    private ?string $name = null;

    #[Duplicate('model', [
        'group' => ['duplicate']
    ])]
    private Collection $chapters = null;
}
yaml
enhavo_resources:
    duplicate:
        App\Entity\Book:
            name:
                type: string
                postfix: ' Copy!!'
                group: ['duplicate']
            chapters:
                type: model
                group: ['duplicate']

Use the manager to duplicate a resource. The returned resource is not saved yet. Use the save method to make it persistent.

php

$otherBook = $this->resourceManager->duplicate($book, ['group' => 'duplicate']);
$this->resourceManager->save($otherBook);

State Machine

The resource bundle uses the winzou state machine. Here is a small graph example. For more documentation read the docs.

yaml
winzou_state_machine:
    app.book:
        class: App\Entity\Book
        property_path: state
        graph: book_graph
        states:
            - new
            - published
        transitions:
            publish:
                from: [new]
                to: published
        # hooks
        callbacks:
            before:
                update_publish_date:
                    on:   'publish'
                    do:   ['@book.manager', 'updatePublishDate']
                    args: ['object']

Use canApplyTransition to check if a transition can be applied or applyTransition to execute the transition with it callbacks and save the state.

php
if ($this->resourceManager->canApplyTransition($book, 'publish', 'book_graph')) {
    $otherBook = $this->resourceManager->applyTransition($book, 'publish', 'book_graph');
}

WARNING

The save hook will not be triggered if you use applyTransition

Grid

A grid is a list of resources. The resource bundle allow highly customized grids. This means, you can redefine your own grid or customize the default one. It comes with a lot of options and subcomponents to fit most of your needs.

Define your grid under enhavo_resource.grids.

yaml
# config/resources/book.yaml
enhavo_resource:
    grids:
        # name of the grid
        app.book: 
            # configuration options
            extends: app.parent_grid
            priority: 10
            overwrite: false
            # default grid class (optional)
            class: Enhavo\Bundle\ResourceBundle\Grid\Grid
            # options of the default grid class
            resource: app.book # resource name (mandatory)
            component: 'grid-grid'
            routes: {} # route options
            collection: {} # collection options
            actions: {} # list of actions
            filters: {} # list of filters
            batches: {}  # list of batches
            columns: {} # list of columns

Configuration control

The options extends, priority and overwrite are not part of the grid class. These options controlling the handling to other configured grids.

If multiple grids defined with the same name, the priority options control the order. Not all options will be overwritten. For example, if you define a action in the parent and another in the child grid, both actions will be used. If you use overwrite with value true in the child, all actions will be overwritten and only the actions in the child definition are used.

With extends, you will inherit the grid class and options from that specified grid.

Grid class and options

By default, the class Enhavo\Bundle\ResourceBundle\Grid\Grid is used and don't need to be defined if you stick to it.

Further options depends on the grid class and it's subsystems.

In the default grid the name is not automatically connected to the resource name. So you are able to define multiple grids for a single resource.

Routing and flow

An admin route is needed to display the resource-index component.

yaml
# config/routes/admin/book.yaml
app_admin_book_index:
    path: /book/index
    defaults:
        _expose: admin
        _vue:
            component: resource-index
            groups: admin
            meta:
                api: app_admin_api_book_index
        _endpoint:
            type: admin

This component will use the api route, which passed over meta and fetch the configuration from this endpoint. Inside the configuration, we find all infos for the subcomponent. The collection subcomponent get a new url to fetch the data for the grid.

yaml
# config/routes/admin_api/book.yaml
app_admin_api_book_index:
    path: /book/index
    methods: [GET]
    defaults:
        _expose: admin_api
        _endpoint:
            type: resource_index
            grid: app.book

app_admin_api_book_list:
    path: /book/list
    methods: [GET,POST]
    defaults:
        _expose: admin_api
        _endpoint:
            type: resource_list
            grid: app.book

app_admin_api_book_batch:
    path: /book/batch
    methods: [POST]
    defaults:
        _expose: admin_api
        _endpoint:
            type: resource_batch
            grid: app.book

Collection

yaml
enhavo_resource:
    grids:
        app.book: 
            collection:
                class: Enhavo\Bundle\ResourceBundle\Collection\TableCollection
                limit:  100,
                paginated: true,
                repository_method: ~
                repository_arguments: ~
                pagination_steps: [5, 10, 50, 100, 500, 1000]
                component: 'collection-table'
                model: 'TableCollection'
                filters: []
                sorting: []
                criteria: []

Action

Actions are like buttons. If you click on one of these buttons, something will be executed. For example an overlay could be displayed etc.

Filter

tbc.

Batch

Batch are like actions, that can be run on multiple items from the table view by selecting the items and choosing an action from a dropdown menu.

Input

An input control the input mask for creating and editing a resource. You have to define all options similar to a grid.

yaml
# config/resources/book.yaml
enhavo_resource:
    inputs:
        # name of the input
        app.book:
            # configuration options
            extends: enhavo_resource.input
            priority: 10
            overwrite: false
            # default input class (optional)
            class: Enhavo\Bundle\ResourceBundle\Input\Input
            # options of the input class
            resource: app.book
            form: App\Form\Type\BookType
            form_options: []
            actions: []
            actions_secondary: []
            tabs: []
            factory_method: createNew
            factory_arguments: []
            repository_method: find
            repository_arguments: [
                'expr:resource.getId()'
            ]
            serialization_groups: endpoint

Routes

Admin routes.

yaml
# config/routes/admin/book.yaml
app_admin_book_create:
    path: /book/create
    defaults:
        _expose: admin
        _vue:
            component: resource-input
            groups: admin
            meta:
                api: app_admin_api_create
        _endpoint:
            type: admin

app_admin_book_update:
    path: /book/update/{id}
    defaults:
        _expose: admin
        _vue:
            component: resource-input
            groups: admin
            meta:
                api: app_admin_api_update
        _endpoint:
            type: admin

Admin api routes.

yaml
# config/routes/admin_api/book.yaml
app_admin_api_create:
    path: /book/create
    methods: [GET, POST]
    defaults:
        _expose: admin_api
        _endpoint:
            type: resource_create
            input: app.book

app_admin_api_update:
    path: /book/update/{id}
    methods: [GET, POST]
    defaults:
        _expose: admin_api
        _endpoint:
            type: resource_update
            input: app.book

Tabs

Define tabs.

yaml
enhavo_resource:
    inputs:
        app.book:
           tabs:
                main:
                    label: Book
                    type: form
                    arrangement: |
                        name
                        chapters

Duplicate

Define the action button.

yaml
enhavo_resource:
    inputs:
        app.book:
           actions:
                duplicate:
                    type: duplicate
                    enabled: 'expr:resource && resource.getId()'

Define the route.

yaml
app_api_book_duplicate:
    path: /book/duplicate/{id}
    methods: [POST]
    defaults:
        _expose: admin_api
        _endpoint:
            type: resource_duplicate
            input: app.book

Preview

Define the preview action.

yaml
enhavo_resource:
    inputs:
        app.book:
           actions:
                preview:
                    type: preview
                    enabled: 'expr:resource && resource.getId()'

Define the route.

yaml
app_admin_api_preview:
    path: /book/preview/{id}
    methods: [GET, POST]
    defaults:
        _expose: admin_api
        _area: theme
        _endpoint:
            type: preview
            input: app.book
            endpoint:
                type: App\Endpoint\BookEndpointType
                resource: expr:resource
                preview: true

Route Resolver

The naming of routes following a convention to prevent configuration (convention over configuration).

Convention

Parts:

  • namespace, normally app or the bundle name
  • area, e.g. theme or admin
  • api if it is an api route
  • resource name or subsystem
  • action

Usage

php
use Enhavo\Bundle\ResourceBundle\RouteResolver\RouteResolverInterface;

class BookEndpointType extends AbstractEndpointType
{
    public function __construct(
        private readonly RouteResolverInterface $routeResolver, 
    )
    {
    }

    public function handleRequest($options, Request $request, Data $data, Context $context): void
    {
        $createRoute = $this->routeResolver->getRoute('create', ['api' => false]); 
        $updateApiRoute = $this->routeResolver->getRoute('update', ['api' => true]); 
    }
}

Expression Language

Usage

php
use Enhavo\Bundle\ResourceBundle\ExpressionLanguage\ResourceExpressionLanguage;

class BookEndpointType extends AbstractEndpointType
{
    public function __construct(
        private readonly ResourceExpressionLanguage $resourceExpression, 
    )
    {
    }

    public function handleRequest($options, Request $request, Data $data, Context $context): void
    {
        $createRoute = $this->resourceExpression->evalute($options['']); 
        $updateApiRoute = $this->routeResolver->evaluteArray($options[''], [ 
            'resource' => $context->get('resource') 
        ]); 
    }
}