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 solid 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
$ composer require enhavo/resource-bundleResource
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.
# 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: 10This 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.
To autowire repositories and factories, default binds are created as well. For the app.book example, you need to name the parameter $bookRepository, $bookFactory or $appBookRepository, $appBookFactory. The parameter type must match the configured ones (Don't use interfaces here).
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.
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->getRepository('app.book');
$book = $repository->find($request->get('id'));
// or factory
$factory = $this->resourceManager->getFactory('app.book');
$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.
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 yaml configuration.
<?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;
}enhavo_resource:
duplicate:
App\Entity\Book:
properties:
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.
$otherBook = $this->resourceManager->duplicate($book, null, ['group' => 'duplicate']);
$this->resourceManager->save($otherBook);It is also possible to duplicate a resource into a target resource. The reference of the target keeps the same, but the values will be changed to the one from the source resource.
$target = $this->resourceManager->duplicate($source, $target, ['group' => 'duplicate']);On the duplicate reference section, you can find the possible duplicate types.
State Machine
The resource bundle uses the winzou state machine. Here is a small graph example. For more documentation read the docs.
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.
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.
# 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 columnsConfiguration 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.
# 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: adminThis 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.
# 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.bookCollection
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.
# 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
validation_groups: ['default']Routes
Admin routes.
# 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: adminAdmin api routes.
# 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.bookTabs
Define tabs.
enhavo_resource:
inputs:
app.book:
tabs:
main:
label: Book
type: form
arrangement: |
name
chaptersDuplicate
Define the action button.
enhavo_resource:
inputs:
app.book:
actions:
duplicate:
type: duplicate
enabled: 'expr:resource && resource.getId()'Define the route.
app_api_book_duplicate:
path: /book/duplicate/{id}
methods: [POST]
defaults:
_expose: admin_api
_endpoint:
type: resource_duplicate
input: app.bookPreview
Define the preview action.
enhavo_resource:
inputs:
app.book:
actions:
preview:
type: preview
enabled: 'expr:resource && resource.getId()'Define the route.
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: trueRoute 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
apiif it is an api route- resource name or subsystem
- action
Examples:
- app_theme_article_show
- app_api_article_show
- enhavo_page_admin_page_index
- enhavo_page_admin_api_page_index
Usage
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
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')
]);
}
}