Cookbook: Creating a Simple CRUD
Here's my attempt to follow the official OroPlatform guide Archive 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:
Copykiat@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
.
Copyinventory_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:
Copykiat@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:
The Datagrid
Define the vehicle datagrid in src\InventoryBundle\Resources\config\oro\datagrids.yml
Copydatagrids: 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/
:
Create a Form Type Service for Vehicle Entity
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
Copyservices: 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:
CopySymfony\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)
When I replaced it with $form = $this->get('form.factory')->create('InventoryBundle\Form\Type\VehicleType', $vehicle);
, the form is rendered correctly:
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 Archive 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 Archive , I needed to make a couple of changes to the file::
- I disable the delete button on line 12.
- I change the variable name from
vehicle
toentity
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, ); } }