Cookbook: Creating a Simple CRUD

Here's my attempt to follow the official OroPlatform guide Show archive.org snapshot on creating a simple CRUD.

Create the InventoryBundle

Starting from a fresh instance of Oro Platform ver 3.1.3, follow the manual steps in Create A Bundle.

Create the Vehicle Entity

Create the file src\InventoryBundle\Entity\Vehicle.php

<?php

namespace InventoryBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 * @ORM\Table(name="vehicle")
 */
class Vehicle
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="string", length=100)
     */
    private $model;

    /**
     * @ORM\Column(type="integer")
     */
    private $seats;

    /**
     * @ORM\Column(name="bought_at", type="date")
     */
    private $boughtAt;

    /**
     * @ORM\Column(name="leased_until", type="date")
     */
    private $leasedUntil;

    public function getId()
    {
        return $this->id;
    }

    public function getModel()
    {
        return $this->model;
    }

    public function setModel($model)
    {
        $this->model = $model;
    }

    public function getSeats()
    {
        return $this->seats;
    }

    public function setSeats($seats)
    {
        $this->seats = $seats;
    }

    public function getBoughtAt()
    {
        return $this->boughtAt;
    }

    public function setBoughtAt($boughtAt)
    {
        $this->boughtAt = $boughtAt;
    }

    public function getLeasedUntil()
    {
        return $this->leasedUntil;
    }

    public function setLeasedUntil($leasedUntil)
    {
        $this->leasedUntil = $leasedUntil;
    }
}

Create the table in DB in bash shell:


kiat@win10 MINGW64 /d/Work/wamp64/www/oro/platform
$ php bin/console doctrine:schema:update --dump-sql

 The following SQL statements will be executed:

     CREATE TABLE vehicle (id INT AUTO_INCREMENT NOT NULL, model VARCHAR(100) NO
T NULL, seats INT NOT NULL, bought_at DATE NOT NULL COMMENT '(DC2Type:date)', le
ased_until DATE NOT NULL COMMENT '(DC2Type:date)', PRIMARY KEY(id)) DEFAULT CHAR
ACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB;

kiat@win10 MINGW64 /d/Work/wamp64/www/oro/platform
$ php bin/console doctrine:schema:update --force

 Updating database schema...

     1 query was executed

 [OK] Database schema updated successfully!

Check that the table vehicle is created in pma.

Create a Basic Controller

Define the route in the file src\InventoryBundle\Resources\config\oro\routing.yml.

inventory_bundle:
    resource: "@InventoryBundle/Controller"
    type: annotation
    prefix: /inventory

The param prefix means that the resource in the URL is prefixed: http://oro2.nks/index_dev.php/inventory.

Create the controller in src\InventoryBundle\Controller\VehicleController.php

<?php
namespace InventoryBundle\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Oro\Bundle\SecurityBundle\Annotation\Acl;

/**
 * @Route("/vehicle")
 */
class VehicleController extends Controller
{
    /**
     * @Route("/", name="inventory.vehicle_index")
     * @Acl(
     *     id="inventory.vehicle_view",
     *     type="entity",
     *     class="InventoryBundle:Vehicle",
     *     permission="VIEW"
     * )
     * @Template
     */
    public function indexAction()
    {
        return array('gridName' => 'vehicles-grid');
    }

}

Refresh the cache before browsing:

kiat@win10 MINGW64 /d/Work/wamp64/www/oro/platform
$ php bin/console cache:clear

 // Clearing the cache for the dev environment with debug
 // true

 [OK] Cache for the "dev" environment (debug=true) was successfully cleared.

To view the datagrid, use the URL http://oro2.nks/index_dev.php/inventory/vehicle/.

Exception:

Unable to find template "InventoryBundle:Vehicle:index.html.twig"

So far so good.

To see that the route is warmed up, click on the Symfony profiler:

index_dev.php > Symfony Profiler > Routing > search for inventory:

Image

The Datagrid

Define the vehicle datagrid in src\InventoryBundle\Resources\config\oro\datagrids.yml

datagrids:
    vehicles-grid:
        source:
            acl_resource: inventory.vehicle_view
            type: orm
            query:
                select:
                    - v.id
                    - v.model
                    - v.seats
                    - v.boughtAt
                    - v.leasedUntil
                from:
                    - { table: InventoryBundle:Vehicle, alias: v }
        columns:
            model:
                label: Model
            seats:
                label: '# Seats'
            boughtAt:
                label: Bought at
                frontend_type: date
            leasedUntil:
                label: Leased until
                frontend_type: date
        properties:
            id: ~
            update_link:
                type: url
                route: inventory.vehicle_update
                params:
                    - id
            view_link:
                type: url
                route: inventory.vehicle_view
                params:
                    - id
            delete_link:
                type: url
                route: inventory_api_delete_vehicle
                params:
                    - id
        sorters:
            columns:
                model:
                    data_name: v.model
                seats:
                    data_name: v.seats
                boughtAt:
                    data_name: v.boughtAt
                leasedUntil:
                    data_name: v.leasedUntil
            default:
                model: ASC
        filters:
            columns:
                model:
                    type: string
                    data_name: v.model
                seats:
                    type: number
                    data_name: v.seats
                boughtAt:
                    type: date
                    data_name: v.boughtAt
                leasedUntil:
                    type: date
                    data_name: v.leasedUntil
        actions:
            view:
                type:          navigate
                label:         View
                link:          view_link
                icon:          eye-open
                acl_resource:  inventory.vehicle_view
                rowAction:     true
            update:
                type:          navigate
                label:         Update
                link:          update_link
                icon:          edit
                acl_resource:  inventory.vehicle_update
            delete:
                type:          delete
                label:         Delete
                link:          delete_link
                icon:          trash
                acl_resource:  inventory.vehicle_delete

Create the View for the Datagrid

src\InventoryBundle\Resources\views\Vehicle\index.html.twig

{% extends 'OroUIBundle:actions:index.html.twig' %}
{% import 'OroUIBundle::macros.html.twig' as UI %}
{% set pageTitle = 'Vehicles'|trans %}

Refresh the cache and navigate to inventory/vehicle/:

Image

Create a Form Type Service for Vehicle Entity

The guide Show archive.org snapshot says:

To be able to create new vehicles and update existing ones, you first have to create a form type and register it as a service:

~~I think it means that a service is required to write to DB.~~ It means that we create a helper class or service for the form. So, let's do that.

Create the Service

Create the form service file src\InventoryBundle\Form\Type\VehicleType.php.

<?php

namespace InventoryBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
//use Symfony\Component\OptionsResolver\OptionsResolverInterface; // Fatal error: Declaration of InventoryBundle\Form\Type\VehicleType::configureOptions(InventoryBundle\Form\Type\OptionsResolver $resolver) must be compatible with Symfony\Component\Form\FormTypeInterface::configureOptions(Symfony\Component\OptionsResolver\OptionsResolver $resolver)
use Symfony\Component\OptionsResolver\OptionsResolver;

class VehicleType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('model')
            ->add('seats')
            ->add('boughtAt')
            ->add('leasedUntil')
        ;
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'InventoryBundle\Entity\Vehicle',
        ));
    }

    public function getName()
    {
        return 'inventory_vehicle';
    }
}

Create the service configuration file in src\InventoryBundle\Resources\config\form.yml

services:
    inventory.form.type.vehicle:
        class: InventoryBundle\Form\Type\VehicleType
        tags:
            - { name: form.type, alias: inventory_vehicle }

Note that most often it is a good idea (especially when you have many services in your bundle) to define form related services in a separate configuration file. By convention form.yml is used but since it’s not autoloaded, we need to load it manually in the bundle extension class.

Load form.yml

The code in the guide is incomplete. Luckily, there are a few references I can use by searching for form.yml in vscode. Eg vendor\oro\platform\src\Oro\Bundle\CommentBundle\DependencyInjection\OroCommentExtension.php.

Create the service extension file src\InventoryBundle\DependencyInjection\InventoryExtension.php

<?php
namespace InventoryBundle\DependencyInjection;

use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;

class InventoryExtension extends Extension
{
    /**
     * {@inheritDoc}
     */
    public function load(array $configs, ContainerBuilder $container)
    {
        $configuration = new Configuration();
        $this->processConfiguration($configuration, $configs);

        $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
        $loader->load('form.yml');
    }
}

I also needed to add src\InventoryBundle\DependencyInjection\Configuration.php to avoid undefined class.

<?php

namespace InventoryBundle\DependencyInjection;

use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;

class Configuration implements ConfigurationInterface
{
    /**
     * {@inheritDoc}
     */
    public function getConfigTreeBuilder()
    {
        $treeBuilder = new TreeBuilder();
        $treeBuilder->root('inventory');

        return $treeBuilder;
    }
}

Refresh the cache and refresh browser to ensure that there is no error.

Render the Vehicle Form for Creating or Updating Entity

Create the form view in src\InventoryBundle\Resources\views\Vehicle\update.html.twig

{% extends 'OroUIBundle:actions:update.html.twig' %}
{% import 'OroUIBundle::macros.html.twig' as UI %}
{% form_theme form with 'OroFormBundle:Form:fields.html.twig' %}

{% if form.vars.value.id %}
    {% set formAction = path('inventory.vehicle_update', { 'id': form.vars.value.id }) %}
{% else %}
    {% set formAction = path('inventory.vehicle_create') %}
{% endif %}

{% block navButtons %}
    {% if form.vars.value.id and is_granted('DELETE', form.vars.value) %}
        {{ UI.deleteButton({
            'dataUrl': path('inventory_api_delete_vehicle', {'id': form.vars.value.id}),
            'dataRedirect': path('inventory.vehicle_index'),
            'aCss': 'no-hash remove-button',
            'id': 'btn-remove-tag',
            'dataId': form.vars.value.id,
            'entity_label': 'Vehicle'|trans
        }) }}
        {{ UI.buttonSeparator() }}
    {% endif %}
    {{ UI.cancelButton(path('inventory.vehicle_index')) }}
    {% set html = UI.saveAndCloseButton() %}
    {% if is_granted('inventory.vehicle_update') %}
        {% set html = html ~ UI.saveAndStayButton() %}
    {% endif %}
    {{ UI.dropdownSaveButton({ 'html': html }) }}
{% endblock navButtons %}

{% block pageHeader %}
    {% if form.vars.value.id %}
        {% set breadcrumbs = {
            'entity':      form.vars.value,
            'indexPath':   path('inventory.vehicle_index'),
            'indexLabel': 'Vehicles'|trans,
            'entityTitle': form.vars.value.model
        } %}
        {{ parent() }}
    {% else %}
        {% set title = 'oro.ui.create_entity'|trans({'%entityName%': 'Vehicle'|trans}) %}
        {% include 'OroUIBundle::page_title_block.html.twig' with { title: title } %}
    {% endif %}
{% endblock pageHeader %}

{% block content_data %}
    {% set id = 'vehicle-edit' %}

    {% set dataBlocks = [{
            'title': 'General'|trans,
            'class': 'active',
            'subblocks': [{
                'title': '',
                'data': [
                    form_row(form.model),
                    form_row(form.seats),
                    form_row(form.boughtAt),
                    form_row(form.leasedUntil),
                ]
            }]
        }]
    %}
    {% set data = {
        'formErrors': form_errors(form)? form_errors(form) : null,
        'dataBlocks': dataBlocks,
    } %}
    {{ parent() }}
{% endblock content_data %}

Add controller actions in src\InventoryBundle\Controller\VehicleController.php

<?php
namespace InventoryBundle\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Oro\Bundle\SecurityBundle\Annotation\Acl;
use Symfony\Component\HttpFoundation\Request;
use InventoryBundle\Entity\Vehicle;

/**
 * @Route("/vehicle")
 */
class VehicleController extends Controller
{
    /**
     * @Route("/", name="inventory.vehicle_index")
     * @Acl(
     *     id="inventory.vehicle_view",
     *     type="entity",
     *     class="InventoryBundle:Vehicle",
     *     permission="VIEW"
     * )
     * @Template
     */
    public function indexAction()
    {
        return array('gridName' => 'vehicles-grid');
    }

    /**
     * @Route("/create", name="inventory.vehicle_create")
     * @Template("InventoryBundle:Vehicle:update.html.twig")
     * @Acl(
     *     id="inventory.vehicle_create",
     *     type="entity",
     *     class="InventoryBundle:Vehicle",
     *     permission="CREATE"
     * )
     */
    public function createAction(Request $request)
    {
        return $this->update(new Vehicle(), $request);
    }

    /**
     * @Route("/update/{id}", name="inventory.vehicle_update", requirements={"id":"\d+"}, defaults={"id":0})
     * @Template()
     * @Acl(
     *     id="inventory.vehicle_update",
     *     type="entity",
     *     class="InventoryBundle:Vehicle",
     *     permission="EDIT"
     * )
     */
    public function updateAction(Vehicle $vehicle, Request $request)
    {
        return $this->update($vehicle, $request);
    }

    private function update(Vehicle $vehicle, Request $request)
    {
        //$form = $this->get('form.factory')->create('inventory_vehicle', $vehicle);
        $form = $this->get('form.factory')->create('InventoryBundle\Form\Type\VehicleType', $vehicle);
        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            $entityManager = $this->getDoctrine()->getManager();
            $entityManager->persist($vehicle);
            $entityManager->flush();

            return $this->get('oro_ui.router')->redirectAfterSave(
                array(
                    'route' => 'inventory.vehicle_update',
                    'parameters' => array('id' => $vehicle->getId()),
                ),
                array('route' => 'inventory.vehicle_index'),
                $vehicle
            );
        }

        return array(
            'entity' => $vehicle,
            'form' => $form->createView(),
        );
    }

}

When I use this $form = $this->get('form.factory')->create('inventory_vehicle', $vehicle);, I encountered error:

Symfony\Component\Form\Exception\InvalidArgumentException:
Could not load type "inventory_vehicle": class does not exist.

  at vendor\symfony\symfony\src\Symfony\Component\Form\FormRegistry.php:86
  at Symfony\Component\Form\FormRegistry->getType('inventory_vehicle')
     (vendor\oro\platform\src\Oro\Bundle\ApiBundle\Form\SwitchableFormRegistry.php:119)
  at Oro\Bundle\ApiBundle\Form\SwitchableFormRegistry->getType('inventory_vehicle')
     (vendor\symfony\symfony\src\Symfony\Component\Form\FormFactory.php:58)
  at Symfony\Component\Form\FormFactory->createBuilder('inventory_vehicle', object(Vehicle), array())
     (vendor\symfony\symfony\src\Symfony\Component\Form\FormFactory.php:30)
  at Symfony\Component\Form\FormFactory->create('inventory_vehicle', object(Vehicle))
     (src\InventoryBundle\Controller\VehicleController.php:63)
  at InventoryBundle\Controller\VehicleController->update(object(Vehicle), object(Request))
     (src\InventoryBundle\Controller\VehicleController.php:43)
  at InventoryBundle\Controller\VehicleController->createAction(object(Request))
     (vendor\symfony\symfony\src\Symfony\Component\HttpKernel\HttpKernel.php:151)
  at Symfony\Component\HttpKernel\HttpKernel->handleRaw(object(Request), 1)
     (vendor\symfony\symfony\src\Symfony\Component\HttpKernel\HttpKernel.php:68)
  at Symfony\Component\HttpKernel\HttpKernel->handle(object(Request), 1, true)
     (vendor\symfony\symfony\src\Symfony\Component\HttpKernel\Kernel.php:200)
  at Symfony\Component\HttpKernel\Kernel->handle(object(Request))
     (public\index_dev.php:32)

Image

When I replaced it with $form = $this->get('form.factory')->create('InventoryBundle\Form\Type\VehicleType', $vehicle);, the form is rendered correctly:

Image

Go ahead and save a vehicle. There are some errors on the return page, something to do with incorrect route:

An exception has been thrown during the rendering of a template ("Unable to generate a URL for the named route "inventory_api_delete_vehicle" as such route does not exist.").

Removing the Delete Entity Vehicle Link

[edit] Follow steps in OroPlatform API and Deleting Entity and API Testing to delete entity.

The guide on deleting entities Show archive.org snapshot is too vague and I don't know how to make it work. It is necessary to disable the delete button in the views in order to render all the pages correctly without error.

src\InventoryBundle\Resources\views\Vehicle\update.html.twig

replace line 12 from

{% form.vars.value.id and is_granted('DELETE', form.vars.value) %}

to

{% if 0 and form.vars.value.id and is_granted('DELETE', form.vars.value) %}

Also comment out the delete_link property in src\InventoryBundle\Resources\config\oro\datagrids.yml:

            #delete_link:
            #    type: url
            #    route: inventory_api_delete_vehicle
            #    params:
            #        - id

Show the Details of the Entity Vehicle

Create the view in src\InventoryBundle\Resources\views\Vehicle\view.html.twig.

Reference the guide Show archive.org snapshot , I needed to make a couple of changes to the file::

  1. I disable the delete button on line 12.
  2. I change the variable name from vehicle to entity to be consistent with parent twig.
{% extends 'OroUIBundle:actions:view.html.twig' %}
{% import 'OroUIBundle::macros.html.twig' as UI %}

{% block navButtons %}
    {% if is_granted('EDIT', entity) %}
        {{ UI.editButton({
            'path' : path('inventory.vehicle_update', { id: entity.id }),
            'entity_label': 'entity'|trans
        }) }}
    {% endif %}

    {% if 0 and is_granted('DELETE', entity) %}
        {{ UI.deleteButton({
            'dataUrl': path('inventory_api_delete_vehicle', {'id': entity.id}),
            'dataRedirect': path('inventory.vehicle_index'),
            'aCss': 'no-hash remove-button',
            'id': 'btn-remove-vehicle',
            'dataId': entity.id,
            'entity_label': 'Vehicle'|trans,
        }) }}
    {% endif %}
{% endblock navButtons %}

{% block pageHeader %}
    {% set breadcrumbs = {
        'entity':      entity,
        'indexPath':   path('inventory.vehicle_index'),
        'indexLabel': 'Vehicles'|trans,
        'entityTitle': entity.model
    } %}
    {{ parent() }}
{% endblock pageHeader %}

{% block content_data %}
    {% set data %}
        <div class="widget-content">
            <div class="row-fluid form-horizontal">
                <div class="responsive-block">
                    {{ UI.renderProperty('Model'|trans, entity.model) }}
                    {{ UI.renderProperty('Seats'|trans, entity.seats) }}
                    {{ UI.renderProperty('Bought at'|trans, entity.boughtAt|date) }}
                    {{ UI.renderProperty('Leased until'|trans, entity.leasedUntil|date) }}
                </div>
            </div>
        </div>
    {% endset %}
    {% set dataBlocks = [
        {
            'title': 'Data'|trans,
            'class': 'active',
            'subblocks': [
                { 'data' : [data] }
            ]
        }
    ] %}

    {% set id = 'vehicleView' %}
    {% set data = { 'dataBlocks': dataBlocks } %}
    {{ parent() }}
{% endblock content_data %}

The final controller file src\InventoryBundle\Controller\VehicleController.php looks like this:

<?php
namespace InventoryBundle\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Oro\Bundle\SecurityBundle\Annotation\Acl;
use Oro\Bundle\SecurityBundle\Annotation\AclAncestor;
use Symfony\Component\HttpFoundation\Request;
use InventoryBundle\Entity\Vehicle;

/**
 * @Route("/vehicle")
 */
class VehicleController extends Controller
{
    /**
     * @Route("/", name="inventory.vehicle_index")
     * @Acl(
     *     id="inventory.vehicle_view",
     *     type="entity",
     *     class="InventoryBundle:Vehicle",
     *     permission="VIEW"
     * )
     * @Template
     */
    public function indexAction()
    {
        return array('gridName' => 'vehicles-grid');
    }

    /**
     * @Route("/create", name="inventory.vehicle_create")
     * @Template("InventoryBundle:Vehicle:update.html.twig")
     * @Acl(
     *     id="inventory.vehicle_create",
     *     type="entity",
     *     class="InventoryBundle:Vehicle",
     *     permission="CREATE"
     * )
     */
    public function createAction(Request $request)
    {
        return $this->update(new Vehicle(), $request);
    }

    /**
     * @Route("/update/{id}", name="inventory.vehicle_update", requirements={"id":"\d+"}, defaults={"id":0})
     * @Template()
     * @Acl(
     *     id="inventory.vehicle_update",
     *     type="entity",
     *     class="InventoryBundle:Vehicle",
     *     permission="EDIT"
     * )
     */
    public function updateAction(Vehicle $vehicle, Request $request)
    {
        return $this->update($vehicle, $request);
    }

    private function update(Vehicle $vehicle, Request $request)
    {
        // $form = $this->get('form.factory')->create('inventory_vehicle', $vehicle); // doesn't work
        // $form = $this->get('form.factory')->create(VehicleType::class, $vehicle); // works with "use InventoryBundle\Form\Type\VehicleType;"
        $form = $this->get('form.factory')->create('InventoryBundle\Form\Type\VehicleType', $vehicle); // works

        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            $entityManager = $this->getDoctrine()->getManager();
            $entityManager->persist($vehicle);
            $entityManager->flush();

            return $this->get('oro_ui.router')->redirectAfterSave(
                array(
                    'route' => 'inventory.vehicle_update',
                    'parameters' => array('id' => $vehicle->getId()),
                ),
                array('route' => 'inventory.vehicle_index'),
                $vehicle
            );
        }

        return array(
            'entity' => $vehicle,
            'form' => $form->createView(),
        );
    }

    /**
     * @Route("/{id}", name="inventory.vehicle_view", requirements={"id"="\d+"})
     * @Template
     * @AclAncestor("inventory.vehicle_view")
     */
    public function viewAction(Vehicle $vehicle)
    {
        return array(
            'entity' => $vehicle,
        );
    }
}

Image

Image

kiatng About 5 years ago