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

Add a universal table picker #714

Merged
merged 24 commits into from
Dec 13, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
45c7e73
Added basic implementation of universal picker
aschempp Sep 3, 2019
cc86ee5
Use universal picker for article alias and teaser element
aschempp Sep 3, 2019
c77cc17
Added pickers to pick content elements
aschempp Sep 3, 2019
4deb3ae
CS and docblocks
aschempp Sep 5, 2019
c2112eb
Feedback applied
aschempp Oct 23, 2019
92ff82a
Implemented a universal picker provider
aschempp Oct 23, 2019
c77847c
Choose checkbox or radio based on "multiple" attribute
aschempp Oct 23, 2019
0913ee6
Correctly implement PickerProviderInterface::createMenuItem
aschempp Oct 23, 2019
ebb87e1
Revert obsolete adjustments to ArticlePickerProvider
aschempp Oct 23, 2019
a370ac4
Make related table method available to child classes
aschempp Oct 23, 2019
31fca99
Refactoring feedback
aschempp Oct 24, 2019
a5cb61e
Run the CS fixer
leofeyer Oct 24, 2019
85aba29
Feedback
aschempp Oct 24, 2019
24cfb59
Add the explanation on GitHub as comment
leofeyer Oct 25, 2019
c3fd0a3
Added abstract DataContainer picker and DC_Table implementation
aschempp Oct 28, 2019
5d638dd
Create a DataContainer instance for callbacks
aschempp Oct 28, 2019
31fec4b
Run the CS fixer
aschempp Oct 28, 2019
c7b1ec4
Renamed abstract class and implemented full unit tests
aschempp Dec 6, 2019
56f9bba
Fixed active state of current picker
aschempp Dec 6, 2019
d16a28f
CS
aschempp Dec 6, 2019
01ff253
Fix the coding style (#1089)
leofeyer Dec 11, 2019
da5c861
Rename "database picker" to "table picker"
leofeyer Dec 13, 2019
43da3fe
Add a unit test for the service configuration
leofeyer Dec 13, 2019
673e558
Merge branch 'master' into feature/universal-picker
leofeyer Dec 13, 2019
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
302 changes: 302 additions & 0 deletions core-bundle/src/Picker/AbstractTablePickerProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,302 @@
<?php

declare(strict_types=1);

/*
* This file is part of Contao.
*
* (c) Leo Feyer
*
* @license LGPL-3.0-or-later
*/

namespace Contao\CoreBundle\Picker;

use Contao\CoreBundle\Framework\ContaoFramework;
use Contao\DcaLoader;
use Doctrine\DBAL\Connection;
use Knp\Menu\FactoryInterface;
use Knp\Menu\ItemInterface;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Contracts\Translation\TranslatorInterface;

abstract class AbstractTablePickerProvider implements PickerProviderInterface, DcaPickerProviderInterface, PickerMenuInterface
{
private const PREFIX = 'dc.';
private const PREFIX_LENGTH = 3;

/**
* @var ContaoFramework
*/
private $framework;

/**
* @var FactoryInterface
*/
private $menuFactory;

/**
* @var RouterInterface
*/
private $router;

/**
* @var TranslatorInterface
*/
private $translator;

/**
* @var Connection
*/
private $connection;

public function __construct(ContaoFramework $framework, FactoryInterface $menuFactory, RouterInterface $router, TranslatorInterface $translator, Connection $connection)
{
$this->framework = $framework;
$this->menuFactory = $menuFactory;
$this->router = $router;
$this->translator = $translator;
$this->connection = $connection;
}

/**
* {@inheritdoc}
*/
public function getUrl(PickerConfig $config): string
{
$table = $this->getTableFromContext($config->getContext());
$modules = $this->getModulesForTable($table);

if (0 === \count($modules)) {
throw new \RuntimeException(sprintf('Table "%s" is not in any back end module (context: %s)', $table, $config->getContext()));
}

$module = array_keys($modules)[0];
[$ptable, $pid] = $this->getPtableAndPid($table, $config->getValue());

if ($ptable) {
foreach ($modules as $key => $tables) {
if (\in_array($ptable, $tables, true)) {
$module = $key;
break;
}
}
}

// If the table is the first in the module, we do not need to add table=xy to the URL
if (0 === array_search($table, $modules[$module], true)) {
return $this->getUrlForValue($config, $module);
}

return $this->getUrlForValue($config, $module, $table, $pid);
}

/**
* {@inheritdoc}
*/
public function addMenuItems(ItemInterface $menu, PickerConfig $config): void
{
$modules = array_keys($this->getModulesForTable($this->getTableFromContext($config->getContext())));

foreach ($modules as $name) {
$params = [
'do' => $name,
'popup' => '1',
'picker' => $config->cloneForCurrent($this->getName().'.'.$name)->urlEncode(),
];

$menu->addChild($this->menuFactory->createItem(
$name,
[
'label' => $this->translator->trans('MOD.'.$name.'.0', [], 'contao_default'),
'linkAttributes' => ['class' => $name],
'current' => $this->isCurrent($config) && $name === substr($config->getCurrent(), \strlen($this->getName().'.')),
'uri' => $this->router->generate('contao_backend', $params),
]
));
}
}

/**
* {@inheritdoc}
*/
public function createMenuItem(PickerConfig $config): ItemInterface
{
$menu = $this->menuFactory->createItem('picker');

$this->addMenuItems($menu, $config);

return $menu->getFirstChild();
}

/**
* {@inheritdoc}
*/
public function supportsContext($context): bool
{
if (0 !== strpos($context, self::PREFIX)) {
return false;
}

$table = $this->getTableFromContext($context);

$this->framework->initialize();
$this->framework->createInstance(DcaLoader::class, [$table])->load();

return $this->getDataContainer() === $GLOBALS['TL_DCA'][$table]['config']['dataContainer']
&& 0 !== \count($this->getModulesForTable($table))
;
}

/**
* {@inheritdoc}
*/
public function supportsValue(PickerConfig $config): bool
{
return true;
}

/**
* {@inheritdoc}
*/
public function isCurrent(PickerConfig $config): bool
{
return 0 === strpos($config->getCurrent(), $this->getName().'.');
}

/**
* {@inheritdoc}
*/
public function getDcaTable(PickerConfig $config = null): string
{
if (null === $config) {
return '';
}

return $this->getTableFromContext($config->getContext());
}

/**
* {@inheritdoc}
*/
public function getDcaAttributes(PickerConfig $config): array
{
$attributes = ['fieldType' => 'radio'];

if ($fieldType = $config->getExtra('fieldType')) {
$attributes['fieldType'] = $fieldType;
}

if ($source = $config->getExtra('source')) {
$attributes['preserveRecord'] = $source;
}

if ($value = $config->getValue()) {
$attributes['value'] = array_map('\intval', explode(',', $value));
}

return $attributes;
}

/**
* {@inheritdoc}
*/
public function convertDcaValue(PickerConfig $config, $value)
{
return (int) $value;
}

protected function getModulesForTable(string $table): array
{
$modules = [];

foreach ($GLOBALS['BE_MOD'] as $v) {
foreach ($v as $name => $module) {
if (
isset($module['tables'])
&& \is_array($module['tables'])
&& \in_array($table, $module['tables'], true)
) {
$modules[$name] = array_values($module['tables']);
}
}
}

return $modules;
}

protected function getTableFromContext(string $context): string
{
return substr($context, self::PREFIX_LENGTH);
}

protected function getUrlForValue(PickerConfig $config, string $module, string $table = null, int $pid = null): string
{
$params = [
'do' => $module,
'popup' => '1',
'picker' => $config->cloneForCurrent($this->getName().'.'.$module)->urlEncode(),
];

if (null !== $table) {
$params['table'] = $table;

if (null !== $pid) {
$params['id'] = $pid;
}
}

return $this->router->generate('contao_backend', $params);
}

protected function getPtableAndPid(string $table, string $value): array
{
// Use the first value if array to find a database record
$id = (int) explode(',', $value)[0];

if (!$value) {
return [null, null];
}

$this->framework->initialize();
$this->framework->createInstance(DcaLoader::class, [$table])->load();

$pid = null;
$ptable = $GLOBALS['TL_DCA'][$table]['config']['ptable'] ?? null;
$dynamicPtable = $GLOBALS['TL_DCA'][$table]['config']['dynamicPtable'] ?? false;

if (!$ptable && !$dynamicPtable) {
return [null, null];
}

$qb = $this->connection->createQueryBuilder();
$qb->select('pid')->from($table)->where($qb->expr()->eq('id', $id));

if ($dynamicPtable) {
$qb->addSelect('ptable');
}

$data = $qb->execute()->fetch();

if (false === $data) {
return [null, null];
}

$pid = (int) $data['pid'];

if ($dynamicPtable) {
$ptable = $data['ptable'] ?: $ptable;

if (!$ptable) {
$ptable = 'tl_article'; // backwards compatibility
}
}

return [$ptable, $pid];
}

/**
* Returns the DataContainer name supported by this picker (e.g. "Table" for DC_Table).
*/
abstract protected function getDataContainer(): string;
}
2 changes: 1 addition & 1 deletion core-bundle/src/Picker/DcaPickerProviderInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ interface DcaPickerProviderInterface extends PickerProviderInterface
*
* @return string
*/
public function getDcaTable();
public function getDcaTable(/* PickerConfig $config */);

/**
* Returns the attributes for the DataContainer.
Expand Down
6 changes: 5 additions & 1 deletion core-bundle/src/Picker/Picker.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,11 @@ public function getMenu(): ItemInterface
$this->menu = $this->menuFactory->createItem('picker');

foreach ($this->providers as $provider) {
$this->menu->addChild($provider->createMenuItem($this->config));
if ($provider instanceof PickerMenuInterface) {
$provider->addMenuItems($this->menu, $this->config);
} else {
$this->menu->addChild($provider->createMenuItem($this->config));
}
}

return $this->menu;
Expand Down
23 changes: 23 additions & 0 deletions core-bundle/src/Picker/PickerMenuInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

/*
* This file is part of Contao.
*
* (c) Leo Feyer
*
* @license LGPL-3.0-or-later
*/

namespace Contao\CoreBundle\Picker;

use Knp\Menu\ItemInterface;

interface PickerMenuInterface
{
/**
* Adds one or more menu items to the picker.
*/
public function addMenuItems(ItemInterface $menu, PickerConfig $config): void;
aschempp marked this conversation as resolved.
Show resolved Hide resolved
}
32 changes: 32 additions & 0 deletions core-bundle/src/Picker/TablePickerProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

/*
* This file is part of Contao.
*
* (c) Leo Feyer
*
* @license LGPL-3.0-or-later
*/

namespace Contao\CoreBundle\Picker;

class TablePickerProvider extends AbstractTablePickerProvider
{
/**
* {@inheritdoc}
*/
public function getName(): string
{
return 'tablePicker';
}

/**
* {@inheritdoc}
*/
protected function getDataContainer(): string
{
return 'Table';
}
}
11 changes: 11 additions & 0 deletions core-bundle/src/Resources/config/services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,17 @@ services:
tags:
- { name: contao.picker_provider, priority: 192 }

contao.picker.table_provider:
class: Contao\CoreBundle\Picker\TablePickerProvider
arguments:
- '@contao.framework'
- '@knp_menu.factory'
- '@router'
- '@translator'
- '@database_connection'
tags:
- { name: contao.picker_provider }

contao.repository.remember_me:
class: Contao\CoreBundle\Repository\RememberMeRepository
arguments:
Expand Down
Loading