PHPackages                             jmf/crud-engine-bundle - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Framework](/categories/framework)
4. /
5. jmf/crud-engine-bundle

ActiveSymfony-bundle[Framework](/categories/framework)

jmf/crud-engine-bundle
======================

CRUD engine bundle for Symfony

8.1.3(1mo ago)018mitPHPPHP &gt;=8.3CI passing

Since Nov 25Pushed 1mo ago1 watchersCompare

[ Source](https://github.com/jmfeurprier/crud-engine-bundle)[ Packagist](https://packagist.org/packages/jmf/crud-engine-bundle)[ RSS](/packages/jmf-crud-engine-bundle/feed)WikiDiscussions 8.1 Synced 4w ago

READMEChangelogDependencies (53)Versions (59)Used By (0)

jmf/crud-engine-bundle
======================

[](#jmfcrud-engine-bundle)

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

Requirements
------------

[](#requirements)

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

Installation
------------

[](#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
-----------

[](#quick-start)

### 1. Configure the bundle

[](#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

[](#2-load-the-routes)

In `config/routes.yaml`:

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

### 3. Create templates (optional)

[](#3-create-templates-optional)

Out of the box, any action whose template is missing renders a built-in **bare** template, so the bundle works immediately. To customize a view, create a Twig template at the configured path (e.g., `templates/article/index.html.twig`, `templates/article/create.html.twig`, etc.) and it takes precedence automatically.

Prefer to fail loudly instead of falling back? Set `schema.view.fallback: fail` (see [Missing templates](#missing-templates)).

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

Actions
-------

[](#actions)

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

ActionDefault Route PathHTTP MethodsFormDescription`index``/articles`GETNoLists all entities`read``/articles/{id}`GETNoDisplays a single entity`create``/articles/new`GET, POSTYesCreates a new entity`update``/articles/{id}/edit`GET, POSTYesUpdates an existing entity`delete``/articles/{id}/delete`GET, POSTNoDeletes 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
-----------------------

[](#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"

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

            # What to do when no form type is configured or discovered:
            #   provide (default) -> build a generic form from the entity's Doctrine metadata
            #   fail              -> throw an exception
            fallback: provide

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

            # What to do when a view template is not found:
            #   provide (default) -> render the bundle's built-in bare template
            #   fail              -> throw an exception
            fallback: provide

    entities:
        App\Entity\Aricle:
            actions:
                index: ~

                read: ~

                create:
                    # Override the form type (optional)
                    form:
                        type: 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

                    # 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

[](#template-placeholders)

Configuration values and default patterns support the following placeholders:

Examples use the entity `App\Entity\BlogPost`: a two-word name, so the casing and pluralization differences are actually visible (with a single-word entity like `Article` they'd all look the same):

PlaceholderExample (`BlogPost`)Description`{{ EntityKey }}``BlogPost`Entity name, PascalCase`{{ EntityKeys }}``BlogPosts`Entity name, pluralized`{{ entityKey }}``blogPost`Entity name, camelCase`{{ entityKeys }}``blogPosts`camelCase, pluralized`{{ entity_key }}``blog_post`Entity name, snake\_case`{{ entity_keys }}``blog_posts`snake\_case, pluralized`{{ entitydashkey }}``blog-post`Entity name, kebab-case`{{ entitydashkeys }}``blog-posts`kebab-case, pluralized (used in URLs)`{{ ActionKey }}``Create`Action name, PascalCase`{{ actionKey }}``create`Action name, camelCase`{{ action_key }}``create`Action name, snake\_case(Actions are always single words: `index`, `read`, `create`, `update`, `delete`, so their case variants only differ in capitalization; the `actionKeys`/`actiondashkey`/… variants exist for symmetry with the entity keys.)

### Overriding placeholders (`schema.keys`)

[](#overriding-placeholders-schemakeys)

These placeholders are not hard-coded: they are themselves defined as Twig patterns under `schema.keys`, and you can override them or add your own:

```
jmf_crud_engine:
    schema:
        keys:
            # Drop pluralization: route paths become "article/..." instead of "articles/...".
            entitydashkeys: "{{ entityClass|u.afterLast('\\\\').kebab }}"
            # Add a custom key, usable as "{{ EntityTitle }}" in any pattern below.
            # "App\Entity\BlogPost" -> "Blog Post"
            EntityTitle: "{{ entityClass|u.afterLast('\\\\').snake.replace({'_': ' '}).title(true) }}"
```

The contract:

- Overrides are **merged over the defaults**: declare only the keys you change.
- A key pattern may reference exactly two **source variables**: `entityClass` (the FQCN) and `action` (the action name, e.g. `create`), using Twig and the [String component `u.*` filters](https://symfony.com/doc/current/string.html#methods-to-change-the-case-of-a-string).
- A key **cannot reference another key** (keys resolve only against the source variables). Doing so fails at container build time with a clear error.
- An unknown placeholder anywhere (a typo such as `{{ entity_keyz }}`) also fails loudly at build time rather than silently rendering empty.

> The runtime placeholder `{{ _entity.id }}` used in `redirection.parameters` is **not** a key: it is resolved later, per request, against the actual entity, so it is unavailable in key/pattern definitions.

Per-entity configuration files (`paths`)
----------------------------------------

[](#per-entity-configuration-files-paths)

Instead of listing every entity under `entities`, you can keep **one file per entity** in a directory. Each file holds just the entity body (the `actions:` block); the entity FQCN is derived from the **filename** prefixed by a base **namespace**, so there is no `jmf_crud_engine:` / `entities:` / `App\Entity\X:` wrapper to repeat.

By default, this is enabled with **zero configuration**: the bundle loads `/packages//` (i.e. `config/packages/jmf_crud_engine/`) with the base namespace `App\Entity`. Just drop files in:

```
# config/packages/jmf_crud_engine/Article.yaml   ->   App\Entity\Article
actions:
    index:
    read:
    create:
        redirection:
            route: article.index
    update:
        redirection:
            route: article.index
    delete:
        redirection:
            route: dashboard
```

The filename (without `.yaml`) is appended to the namespace, so `Article.yaml` → `App\Entity\Article`. Subdirectories map to sub-namespaces (`Blog/Post.yaml` → `App\Entity\Blog\Post`).

### Customizing the directories

[](#customizing-the-directories)

Override `paths` to point elsewhere or to scan several directories (each with its own base namespace). A leading empty `namespace` makes the directory layout mirror the full FQCN (`App/Entity/Article.yaml`).

```
jmf_crud_engine:
    paths:
        - path: '%kernel.project_dir%/config/crud'
          namespace: 'App\Entity'
        - path: '%kernel.project_dir%/config/crud-admin'
          namespace: 'App\Entity\Admin'
```

Setting `paths` explicitly **replaces** the default. `paths` and inline `entities` can be used together, but a given entity class must be defined in exactly one place: declaring it both via a file and inline (or across two scanned directories) throws `CrudEngineDuplicateEntityException` at container build time.

### Notes

[](#notes)

- Files support `!php/const ...` (e.g. a route `requirements` value) and the empty-action shorthand (`read:` with no body), exactly like inline config.
- As with inline config, the result is compiled into the container, so adding/editing/removing a file requires a container rebuild (`cache:clear`) to take effect.

Action Helpers
--------------

[](#action-helpers)

Action helpers allow you to customize behavior at specific lifecycle hooks without replacing the entire controller. Create a class extending the matching `*ActionHelperBase` and register it as a Symfony service. Each base implements the full interface with sensible defaults (the same behavior the bundle uses when no helper is configured), so you override only the hooks you actually need.

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.

> The configuration (route names/paths, form type and helper auto-discovery, view paths, etc.) is resolved once, at container build time, and cached in the compiled container. As with routes, adding a helper/form-type class that matches a discovery pattern requires a container rebuild (`cache:clear`) to be picked up.

### Create Helper

[](#create-helper)

```
use Jmf\CrudEngine\Controller\Helpers\CreateActionHelperBase;
use Override;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request;

/**
 * @extends CreateActionHelperBase
 */
class ArticleCreateActionHelper extends CreateActionHelperBase
{
    /**
     * Instantiate the new entity. The base does `new $entityClass()`; override only if the
     * entity needs constructor arguments. (The zero-config default uses the Doctrine Instantiator.)
     */
    #[Override]
    public function createEntity(Request $request, string $entityClass): object
    {
        return new Article();
    }

    /**
     * Called before the entity is persisted.
     */
    #[Override]
    public function hookBeforePersist(Request $request, object $entity, FormInterface $form): void
    {
        // e.g. set timestamps, assign an owner
    }

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

The base also provides `persist()` (Doctrine `persist` + `flush`) and `hookAfterPersist()`. Override them only to change persistence or run post-save side effects.

### Update Helper

[](#update-helper)

```
use Jmf\CrudEngine\Controller\Helpers\UpdateActionHelperBase;
use Override;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request;

/**
 * @extends UpdateActionHelperBase
 */
class ArticleUpdateActionHelper extends UpdateActionHelperBase
{
    /**
     * Called before the updated entity is persisted.
     */
    #[Override]
    public function hookBeforePersist(Request $request, object $entity, FormInterface $form): void
    {
        // e.g. bump an "updated at" timestamp
    }

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

Same hooks as the create helper, minus `createEntity` (the entity already exists). The default `persist()` here only `flush`es the already-managed entity; `hookAfterPersist()` is also available for post-save side effects.

### Index Helper

[](#index-helper)

```
use Doctrine\Persistence\ObjectManager;
use Jmf\CrudEngine\Controller\Helpers\IndexActionHelperBase;
use Override;
use Symfony\Component\HttpFoundation\Request;

/**
 * @extends IndexActionHelperBase
 */
class ArticleIndexActionHelper extends IndexActionHelperBase
{
    /**
     * Return a custom collection. The base does `getRepository($entityClass)->findAll()`.
     */
    #[Override]
    public function getEntities(Request $request, string $entityClass, ObjectManager $objectManager): iterable
    {
        return $objectManager->getRepository($entityClass)->findBy(['published' => true]);
    }

    #[Override]
    public function getViewVariables(Request $request): array
    {
        return [];
    }
}
```

### Read Helper

[](#read-helper)

```
use Jmf\CrudEngine\Controller\Helpers\ReadActionHelperBase;
use Override;
use Symfony\Component\HttpFoundation\Request;

/**
 * @extends ReadActionHelperBase
 */
class ArticleReadActionHelper extends ReadActionHelperBase
{
    #[Override]
    public function getViewVariables(Request $request, object $entity): array
    {
        return [];
    }
}
```

### Delete Helper

[](#delete-helper)

```
use Jmf\CrudEngine\Controller\Helpers\DeleteActionHelperBase;
use Override;
use Symfony\Component\HttpFoundation\Request;

/**
 * @extends DeleteActionHelperBase
 */
class ArticleDeleteActionHelper extends DeleteActionHelperBase
{
    #[Override]
    public function hookBeforeRemove(object $entity): void
    {
        // e.g. guard against deleting a referenced entity
    }

    #[Override]
    public function getViewVariables(Request $request, object $entity): array
    {
        return [];
    }
}
```

The base also provides `remove()` (Doctrine `remove` + `flush`), `hookAfterRemove()`, and `onFailure()` (re-throws the failure). Override them as needed.

Templates
---------

[](#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`

[](#example-templatesarticleindexhtmltwig)

```
{% for article in articles %}
    {{ article.title }}
{% endfor %}
```

### Example: `templates/article/create.html.twig`

[](#example-templatesarticlecreatehtmltwig)

```
{{ form_start(form) }}
{{ form_widget(form) }}
Create
{{ form_end(form) }}
```

### Missing templates

[](#missing-templates)

When the resolved template for an action does not exist, the behavior is controlled by `schema.view.fallback`:

ValueBehavior`provide` (default)Renders the bundle's built-in bare template (`@JmfCrudEngine/{action}.html.twig`).`fail`Throws `CrudEngineMissingViewException`.The built-in templates are intentionally minimal: they exist to get pages rendering immediately and to be overridden. Providing your own template at the configured path always takes precedence over the built-in one.

### Missing form types

[](#missing-form-types)

For `create`/`update`, when no form type is configured (`form.type`) or discovered (via the `schema.form.type` patterns), the behavior is controlled by `schema.form.fallback`:

ValueBehavior`provide` (default)Builds a generic form from the entity's Doctrine metadata (`CrudEngineEntityType`).`fail`Throws `CrudEngineMissingConfigurationException`.The generic form is a scaffold to be overridden: it maps scalar columns and `enumType` fields, and renders to-one associations as a choice of related entities; it skips identifiers, embeddables, to-many associations, and unmappable column types. Configuring or discovering a real form type always takes precedence.

To make the fallback visible wherever it renders (including under a custom template), the generated form prepends a disabled, read-only "Generated fallback form" field naming the form type class to implement to replace it.

License
-------

[](#license)

MIT

###  Health Score

49

—

FairBetter than 94% of packages

Maintenance90

Actively maintained with recent releases

Popularity9

Limited adoption so far

Community7

Small or concentrated contributor base

Maturity76

Established project with proven stability

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~23 days

Recently: every ~2 days

Total

58

Last Release

48d ago

Major Versions

5.0.0 → 6.0.02024-04-09

4.3.1 → 5.0.12025-04-03

5.0.x-dev → 6.0.12025-04-03

6.0.x-dev → 7.0.0-rc12025-07-18

7.4.x-dev → 8.0.12026-06-09

PHP version history (3 changes)1.0.0-rc1PHP &gt;=7.4

2.0.0PHP &gt;=8.1

4.0.0PHP &gt;=8.3

### Community

Maintainers

![](https://www.gravatar.com/avatar/6b08b8105ec8bc88d1c911e10dbf156814769650a67960e2f3d71ed6d2417f0d?d=identicon)[jmfeurprier](/maintainers/jmfeurprier)

---

Top Contributors

[![jmfeurprier](https://avatars.githubusercontent.com/u/8434690?v=4)](https://github.com/jmfeurprier "jmfeurprier (196 commits)")

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan, Rector

Code StylePHP\_CodeSniffer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/jmf-crud-engine-bundle/health.svg)

```
[![Health](https://phpackages.com/badges/jmf-crud-engine-bundle/health.svg)](https://phpackages.com/packages/jmf-crud-engine-bundle)
```

###  Alternatives

[sylius/sylius

E-Commerce platform for PHP, based on Symfony framework.

8.5k6.0M766](/packages/sylius-sylius)[easycorp/easyadmin-bundle

Admin generator for Symfony applications

4.3k18.3M414](/packages/easycorp-easyadmin-bundle)[pimcore/pimcore

Content &amp; Product Management Framework (CMS/PIM/E-Commerce)

3.8k3.9M532](/packages/pimcore-pimcore)[contao/core-bundle

Contao Open Source CMS

1301.7M3.0k](/packages/contao-core-bundle)[open-dxp/opendxp

Content &amp; Product Management Framework (CMS/PIM)

9626.1k65](/packages/open-dxp-opendxp)[prestashop/prestashop

PrestaShop is an Open Source e-commerce platform, committed to providing the best shopping cart experience for both merchants and customers.

9.2k18.1k](/packages/prestashop-prestashop)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
