Here's my attempt to follow the official OroPlatform guide Show archive.org snapshot on creating a simple CRUD.
Starting from a fresh instance of Oro Platform ver 3.1.3, follow the manual steps in Create A Bundle.
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.
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:
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
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/
:
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 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.
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.
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)
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.").
[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
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::
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,
);
}
}