Posted over 1 year ago. Visible to the public. Linked content.

Cookbook: Creating a Simple CRUD

Here's my attempt to follow the official OroPlatform guide 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

Copy
<?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:

Copy
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.

Copy
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

Copy
<?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:

Copy
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

Copy
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

Copy
{% 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 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.

Copy
<?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

Copy
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

Copy
<?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.

Copy
<?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

Copy
{% 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

Copy
<?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:

Copy
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 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:

Copy
#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, 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.
Copy
{% 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:

Copy
<?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

Owner of this card:

Avatar
kiatng
Last edit:
over 1 year ago
by kiatng
Attachments:
emptygrid.png, error-create-vehicle.png, profiler-routing.png, vehicle-form.png, vehicle-list.png, vehicle-view.png
Posted by kiatng to Oro
This website uses cookies to improve usability and analyze traffic.
Accept or learn more