Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Example] Integrate custom entity into the administration interface #84

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions config/forms/event_details.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?xml version="1.0" ?>
<form xmlns="http://schemas.sulu.io/template/template"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://schemas.sulu.io/template/template http://schemas.sulu.io/template/form-1.0.xsd"
>
<key>event_details</key>

<properties>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The form view can be structured by using <section> tags in the form configuration. An example can be found in the form configuration of the built-in contact entity.

<property name="name" type="text_line" mandatory="true" colspan="12">
<meta>
<title>sulu_admin.name</title>
</meta>

<params>
<param name="headline" value="true"/>
</params>
</property>

<property name="image" type="single_media_selection" colspan="12">
<meta>
<title>app.image</title>
</meta>
</property>

<property name="startDate" type="date" colspan="6">
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Available values for the type attribute of <property> tags are printed to the development console of the browser when an invalid type such as invalid-property-type is used in a form configuration.

<meta>
<title>app.start_date</title>
</meta>
</property>

<property name="endDate" type="date" colspan="6">
<meta>
<title>app.end_date</title>
</meta>
</property>
</properties>
</form>
30 changes: 30 additions & 0 deletions config/lists/events.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?xml version="1.0" ?>
<list xmlns="http://schemas.sulu.io/list-builder/list">
<key>events</key>

<properties>
Copy link
Contributor Author

@niklasnatter niklasnatter Feb 3, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

List configurations can include fields of associated entities by defining <join> tags. An example can be found in the list configuration of the built-in contact entity.

<property name="id" visibility="no" translation="sulu_admin.id">
<field-name>id</field-name>
<entity-name>App\Entity\Event</entity-name>
</property>

<property name="name" visibility="always" searchability="yes" translation="sulu_admin.name">
<field-name>name</field-name>
<entity-name>App\Entity\Event</entity-name>
</property>

<property name="startDate" visibility="yes" translation="app.start_date" type="date">
Copy link
Contributor Author

@niklasnatter niklasnatter Feb 3, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Available values for the type attribute of <property> tags are printed to the development console of the browser when an invalid type such as invalid-property-type is used in a list configuration.

<field-name>startDate</field-name>
<entity-name>App\Entity\Event</entity-name>

<filter type="datetime" />
</property>

<property name="endDate" visibility="yes" translation="app.end_date" type="date">
<field-name>endDate</field-name>
<entity-name>App\Entity\Event</entity-name>

<filter type="datetime" />
</property>
</properties>
</list>
6 changes: 6 additions & 0 deletions config/packages/app_event_admin.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
sulu_admin:
resources:
events:
routes:
list: 'app.get_event_list'
detail: 'app.get_event'
133 changes: 133 additions & 0 deletions src/Admin/EventAdmin.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
<?php

declare(strict_types=1);

namespace App\Admin;

use App\Entity\Event;
use Sulu\Bundle\AdminBundle\Admin\Admin;
use Sulu\Bundle\AdminBundle\Admin\Navigation\NavigationItem;
use Sulu\Bundle\AdminBundle\Admin\Navigation\NavigationItemCollection;
use Sulu\Bundle\AdminBundle\Admin\View\ToolbarAction;
use Sulu\Bundle\AdminBundle\Admin\View\ViewBuilderFactoryInterface;
use Sulu\Bundle\AdminBundle\Admin\View\ViewCollection;
use Sulu\Component\Security\Authorization\PermissionTypes;
use Sulu\Component\Security\Authorization\SecurityCheckerInterface;

class EventAdmin extends Admin
{
public const LIST_VIEW = 'app.event.list';
public const ADD_FORM_VIEW = 'app.event.add_form';
public const ADD_FORM_DETAILS_VIEW = 'app.event.add_form.details';
public const EDIT_FORM_VIEW = 'app.event.edit_form';
public const EDIT_FORM_DETAILS_VIEW = 'app.event.edit_form.details';

private ViewBuilderFactoryInterface $viewBuilderFactory;
private SecurityCheckerInterface $securityChecker;

public function __construct(
ViewBuilderFactoryInterface $viewBuilderFactory,
SecurityCheckerInterface $securityChecker
) {
$this->viewBuilderFactory = $viewBuilderFactory;
$this->securityChecker = $securityChecker;
}

public function configureNavigationItems(NavigationItemCollection $navigationItemCollection): void
{
if ($this->securityChecker->hasPermission(Event::SECURITY_CONTEXT, PermissionTypes::EDIT)) {
$rootNavigationItem = new NavigationItem('app.events');
$rootNavigationItem->setIcon('su-calendar');
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Available icons are documented in the Sulu Javascript Docs.

$rootNavigationItem->setPosition(30);
$rootNavigationItem->setView(static::LIST_VIEW);

$navigationItemCollection->add($rootNavigationItem);
}
}

public function configureViews(ViewCollection $viewCollection): void
{
$formToolbarActions = [];
$listToolbarActions = [];

if ($this->securityChecker->hasPermission(Event::SECURITY_CONTEXT, PermissionTypes::ADD)) {
$listToolbarActions[] = new ToolbarAction('sulu_admin.add');
}

if ($this->securityChecker->hasPermission(Event::SECURITY_CONTEXT, PermissionTypes::EDIT)) {
$formToolbarActions[] = new ToolbarAction('sulu_admin.save');
}

if ($this->securityChecker->hasPermission(Event::SECURITY_CONTEXT, PermissionTypes::DELETE)) {
$formToolbarActions[] = new ToolbarAction('sulu_admin.delete');
$listToolbarActions[] = new ToolbarAction('sulu_admin.delete');
}

if ($this->securityChecker->hasPermission(Event::SECURITY_CONTEXT, PermissionTypes::VIEW)) {
$listToolbarActions[] = new ToolbarAction('sulu_admin.export');
}

if ($this->securityChecker->hasPermission(Event::SECURITY_CONTEXT, PermissionTypes::EDIT)) {
$viewCollection->add(
$this->viewBuilderFactory->createListViewBuilder(static::LIST_VIEW, '/events')
->setResourceKey(Event::RESOURCE_KEY)
->setListKey(Event::LIST_KEY)
->setTitle('app.events')
->addListAdapters(['table'])
->setAddView(static::ADD_FORM_VIEW)
->setEditView(static::EDIT_FORM_VIEW)
->addToolbarActions($listToolbarActions)
);

$viewCollection->add(
$this->viewBuilderFactory->createResourceTabViewBuilder(static::ADD_FORM_VIEW, '/events/add')
->setResourceKey(Event::RESOURCE_KEY)
->setBackView(static::LIST_VIEW)
);

$viewCollection->add(
$this->viewBuilderFactory->createFormViewBuilder(static::ADD_FORM_DETAILS_VIEW, '/details')
->setResourceKey(Event::RESOURCE_KEY)
->setFormKey(Event::FORM_KEY)
->setTabTitle('sulu_admin.details')
->setEditView(static::EDIT_FORM_VIEW)
->addToolbarActions($formToolbarActions)
->setParent(static::ADD_FORM_VIEW)
);

$viewCollection->add(
$this->viewBuilderFactory->createResourceTabViewBuilder(static::EDIT_FORM_VIEW, '/events/:id')
->setResourceKey(Event::RESOURCE_KEY)
->setBackView(static::LIST_VIEW)
);

$viewCollection->add(
$this->viewBuilderFactory->createFormViewBuilder(static::EDIT_FORM_DETAILS_VIEW, '/details')
->setResourceKey(Event::RESOURCE_KEY)
->setFormKey(Event::FORM_KEY)
->setTabTitle('sulu_admin.details')
->addToolbarActions($formToolbarActions)
->setParent(static::EDIT_FORM_VIEW)
);
}
}

/**
* @return mixed[]
*/
public function getSecurityContexts(): array
{
return [
self::SULU_ADMIN_SECURITY_SYSTEM => [
'Events' => [
Event::SECURITY_CONTEXT => [
PermissionTypes::VIEW,
PermissionTypes::ADD,
PermissionTypes::EDIT,
PermissionTypes::DELETE,
],
],
],
];
}
}
157 changes: 157 additions & 0 deletions src/Controller/Admin/EventController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
<?php

declare(strict_types=1);

namespace App\Controller\Admin;

use App\Common\DoctrineListRepresentationFactory;
use App\Entity\Event;
use Doctrine\ORM\EntityManagerInterface;
use Sulu\Bundle\MediaBundle\Media\Manager\MediaManagerInterface;
use Sulu\Component\Security\SecuredControllerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Annotation\Route;

/**
* @phpstan-type EventData array{
* id: int|null,
* name: string,
* image: array{id: int}|null,
* startDate: string|null,
* endDate: string|null,
* }
*/
class EventController extends AbstractController implements SecuredControllerInterface
{
private DoctrineListRepresentationFactory $doctrineListRepresentationFactory;
private EntityManagerInterface $entityManager;
private MediaManagerInterface $mediaManager;

public function __construct(
DoctrineListRepresentationFactory $doctrineListRepresentationFactory,
EntityManagerInterface $entityManager,
MediaManagerInterface $mediaManager
) {
$this->doctrineListRepresentationFactory = $doctrineListRepresentationFactory;
$this->entityManager = $entityManager;
$this->mediaManager = $mediaManager;
}

/**
* @Route("/admin/api/events/{id}", methods={"GET"}, name="app.get_event")
*/
public function getAction(int $id): Response
{
$event = $this->entityManager->getRepository(Event::class)->find($id);
if (!$event) {
throw new NotFoundHttpException();
}

return $this->json($this->getDataForEntity($event));
}

/**
* @Route("/admin/api/events/{id}", methods={"PUT"}, name="app.put_event")
*/
public function putAction(Request $request, int $id): Response
{
$event = $this->entityManager->getRepository(Event::class)->find($id);
if (!$event) {
throw new NotFoundHttpException();
}

/** @var EventData $data */
$data = $request->toArray();
$this->mapDataToEntity($data, $event);
$this->entityManager->flush();

return $this->json($this->getDataForEntity($event));
}

/**
* @Route("/admin/api/events", methods={"POST"}, name="app.post_event")
*/
public function postAction(Request $request): Response
{
$event = new Event();

/** @var EventData $data */
$data = $request->toArray();
$this->mapDataToEntity($data, $event);
$this->entityManager->persist($event);
$this->entityManager->flush();

return $this->json($this->getDataForEntity($event), 201);
}

/**
* @Route("/admin/api/events/{id}", methods={"DELETE"}, name="app.delete_event")
*/
public function deleteAction(int $id): Response
{
/** @var Event $event */
$event = $this->entityManager->getReference(Event::class, $id);
$this->entityManager->remove($event);
$this->entityManager->flush();

return $this->json(null, 204);
}

/**
* @Route("/admin/api/events", methods={"GET"}, name="app.get_event_list")
*/
public function getListAction(): Response
{
$listRepresentation = $this->doctrineListRepresentationFactory->createDoctrineListRepresentation(
Event::RESOURCE_KEY
);

return $this->json($listRepresentation->toArray());
}

/**
* @return EventData $data
*/
protected function getDataForEntity(Event $entity): array
{
$image = $entity->getImage();
$startDate = $entity->getStartDate();
$endDate = $entity->getEndDate();

return [
'id' => $entity->getId(),
'name' => $entity->getName(),
'image' => $image
? ['id' => $image->getId()]
: null,
'startDate' => $startDate ? $startDate->format('c') : null,
'endDate' => $endDate ? $endDate->format('c') : null,
];
}

/**
* @param EventData $data
*/
protected function mapDataToEntity(array $data, Event $entity): void
{
$imageId = $data['image']['id'] ?? null;

$entity->setName($data['name']);
$entity->setImage($imageId ? $this->mediaManager->getEntityById($imageId) : null);
$entity->setStartDate($data['startDate'] ? new \DateTimeImmutable($data['startDate']) : null);
$entity->setEndDate($data['endDate'] ? new \DateTimeImmutable($data['endDate']) : null);
}

public function getSecurityContext(): string
{
return Event::SECURITY_CONTEXT;
}

public function getLocale(Request $request): ?string
{
return $request->query->get('locale');
}
}
Loading