jmf/crud-engine-bundle

CRUD engine bundle for Symfony

Maintainers

Package info

github.com/jmfeurprier/crud-engine-bundle

Type:symfony-bundle

pkg:composer/jmf/crud-engine-bundle

Statistics

Installs: 475

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

7.4.0 2026-05-31 06:58 UTC

README

A Symfony bundle that automates CRUD (Create, Read, Update, Delete) controllers and routes for Doctrine entities based on configuration, eliminating repetitive boilerplate code.

Requirements

  • PHP 8.3+
  • Symfony 7.0 or 8.0
  • Doctrine ORM 3.0+

Installation

composer require jmf/crud-engine-bundle

Register the bundle in config/bundles.php if not using Symfony Flex:

return [
    // ...
    Jmf\CrudEngine\JmfCrudEngineBundle::class => ['all' => true],
];

Quick Start

1. Configure the bundle

Create config/packages/jmf_crud_engine.yaml:

jmf_crud_engine:
    entities:
        App\Entity\Article:
            actions:
                create:
                    redirection:
                        route: article.index
                delete:
                    redirection:
                        route: dashboard
                index:
                read:
                update:
                    redirection:
                        route: article.index

2. Load the routes

In config/routes.yaml:

jmf_crud_engine:
    resource: 'Jmf\CrudEngine\Routing\RouteLoader'
    type: service

3. Create templates

Create Twig templates for each action (e.g., templates/article/index.html.twig, templates/article/create.html.twig, etc.).

That's it — the bundle automatically registers routes and wires up controllers for all configured actions.

Actions

The bundle provides five actions, each mapped to a controller:

Action Default Route Path HTTP Methods Form Description
index /articles GET No Lists all entities
read /articles/{id} GET No Displays a single entity
create /articles/new GET, POST Yes Creates a new entity
update /articles/{id}/edit GET, POST Yes Updates an existing entity
delete /articles/{id}/delete GET, POST No Deletes an entity (POST confirms)

Route paths and names are derived from the entity class name by default and can be overridden via configuration.

Configuration Reference

jmf_crud_engine:
    schema:
        # Default patterns for auto-discovering helper classes (Twig-style placeholders)
        helper:
            - "App\\Controller\\{{ EntityKey }}\\{{ ActionKey }}ActionHelper"
            - "App\\Controller\\{{ EntityKey }}{{ ActionKey }}ActionHelper"

        # Default patterns for auto-discovering form types
        formType:
            - "App\\Form\\{{ EntityKey }}\\{{ ActionKey }}Type"
            - "App\\Form\\{{ EntityKey }}{{ ActionKey }}Type"
            - "App\\Form\\{{ EntityKey }}Type"

        # Default view path pattern
        view:
            path: "{{ entity_key }}/{{ action_key }}.html.twig"

    entities:
        App\Entity\Aricle:
            # Roles required for all actions on this entity (optional)
            roles:
                - ROLE_ADMIN

            actions:
                index: ~

                read: ~

                create:
                    # Override the form type (optional)
                    formType: App\Form\Article\CreateType

                    # Override the helper service (optional)
                    helper: App\Controller\Article\CreateActionHelper

                    # Redirect after a successful form submission (required for create/update/delete)
                    redirection:
                        route: article.index
                        parameters:
                            id: "{{ _entity.id }}"  # Twig expression using the entity
                        fragment: section            # Optional URL fragment

                    # Roles required for this action only (optional, overrides entity roles)
                    roles:
                        - ROLE_EDITOR

                    # Override route configuration (optional)
                    route:
                        path: /blog/new
                        parameters: {}
                        requirements:
                            id: '\d+'

                    # Override template configuration (optional)
                    view:
                        path: article/create.html.twig
                        variables:
                            # Map template variable names to alternatives accepted in the template
                            form: [articleForm, newArticleForm]

                update:
                    redirection:
                        route: article.index

                delete:
                    redirection:
                        route: dashboard

Template Placeholders

Configuration values and default patterns support the following placeholders:

Placeholder Example (Article) Description
{{ EntityKey }} Article Entity class name
{{ EntityKeys }} Articles Pluralized entity class name
{{ entityKey }} article Camel-cased entity key
{{ entity_key }} article Snake-cased entity key
{{ ActionKey }} Create Action name (Pascal case)
{{ actionKey }} create Action name (camel case)
{{ action_key }} create Action name (snake case)

Action Helpers

Action helpers allow you to customize behavior at specific lifecycle hooks without replacing the entire controller. Create a class implementing the appropriate interface and register it as a Symfony service.

If the class name matches a configured default pattern (e.g., App\Controller\Article\CreateActionHelper), it is picked up automatically. Otherwise, set helper explicitly in the action configuration.

Create / Update Helper

use Jmf\CrudEngine\Controller\Helpers\CreateActionHelperInterface;

class ArticleCreateActionHelper implements CreateActionHelperInterface
{
    /**
     * Instantiate the new entity (optional — skipping uses Doctrine Instantiator).
     */
    public function createEntity(): object
    {
        return new Article();
    }

    /**
     * Called before the entity is persisted.
     */
    public function hookBeforePersist(object $entity, array $parameters): void
    {
        // e.g. set timestamps, assign an owner
    }

    /**
     * Persist the entity (optional — skipping uses default EntityManager::persist + flush).
     */
    public function persist(object $entity): void
    {
        // custom persistence logic
    }

    /**
     * Called after the entity is persisted.
     */
    public function hookAfterPersist(object $entity, array $parameters): void
    {
        // e.g. dispatch domain events
    }

    /**
     * Extra variables passed to the template.
     */
    public function getViewVariables(object $entity, array $parameters): array
    {
        return [];
    }
}

Index Helper

use Jmf\CrudEngine\Controller\Helpers\IndexActionHelperInterface;

class ArticleIndexActionHelper implements IndexActionHelperInterface
{
    public function getEntities(array $parameters): iterable
    {
        // return custom entity collection
    }

    public function hookBeforeRender(iterable $entities, array $parameters): void { }

    public function getViewVariables(iterable $entities, array $parameters): array
    {
        return [];
    }
}

Read Helper

use Jmf\CrudEngine\Controller\Helpers\ReadActionHelperInterface;

class ArticleReadActionHelper implements ReadActionHelperInterface
{
    public function getViewVariables(object $entity, array $parameters): array
    {
        return [];
    }
}

Delete Helper

use Jmf\CrudEngine\Controller\Helpers\DeleteActionHelperInterface;

class ArticleDeleteActionHelper implements DeleteActionHelperInterface
{
    public function hookBeforeRemove(object $entity, array $parameters): void { }
    public function remove(object $entity): void { }
    public function hookAfterRemove(object $entity, array $parameters): void { }
    public function onFailure(object $entity, array $parameters): \Symfony\Component\HttpFoundation\Response { }
    public function getViewVariables(object $entity, array $parameters): array { return []; }
}

Templates

Templates receive the entity (or collection) as a variable. The variable name is derived from the entity key (e.g., article for App\Entity\Article, articles for the index action).

Forms are passed as form (a FormView instance).

Example: templates/article/index.html.twig

{% for article in articles %}
    <h2>{{ article.title }}</h2>
{% endfor %}

Example: templates/article/create.html.twig

{{ form_start(form) }}
{{ form_widget(form) }}
<button type="submit">Create</button>
{{ form_end(form) }}

Security

Access can be restricted at entity level or per-action using roles:

jmf_crud_engine:
    entities:
        App\Entity\Article:
            roles: [ROLE_ADMIN]          # applies to all actions
            actions:
                create:
                    roles: [ROLE_EDITOR] # overrides entity-level roles for this action

License

MIT