Skip to content

Commit dd7b35a

Browse files
committed
forwardport alias field
1 parent 99f03fe commit dd7b35a

File tree

4 files changed

+349
-0
lines changed

4 files changed

+349
-0
lines changed

src/Dca/AliasField.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
namespace HeimrichHannot\UtilsBundle\Dca;
4+
5+
class AliasField extends AbstractDcaField
6+
{
7+
private static $tables = [];
8+
9+
protected static function storeConfig(DcaFieldConfiguration $config): void
10+
{
11+
static::$tables[$config->getTable()] = $config;
12+
}
13+
14+
protected static function loadConfig(): array
15+
{
16+
return static::$tables;
17+
}
18+
19+
protected static function createOptionObject(string $table): DcaFieldConfiguration
20+
{
21+
return new AliasFieldConfiguration($table);
22+
}
23+
24+
public static function getField(): array
25+
{
26+
return [
27+
'exclude' => true,
28+
'search' => true,
29+
'inputType' => 'text',
30+
'eval' => ['rgxp' => 'alias', 'unique' => true, 'maxlength' => 128, 'tl_class' => 'w50'],
31+
'save_callback' => [],
32+
'sql' => "varchar(255) BINARY NOT NULL default ''",
33+
];
34+
}
35+
}

src/Dca/AliasFieldConfiguration.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
namespace HeimrichHannot\UtilsBundle\Dca;
4+
5+
use HeimrichHannot\UtilsBundle\EventListener\DcaField\AliasDcaFieldListener;
6+
7+
class AliasFieldConfiguration extends DcaFieldConfiguration
8+
{
9+
public ?array $aliasExistCallback = [AliasDcaFieldListener::class, 'onFieldsAliasSaveCallback'];
10+
11+
public string $fieldName = 'alias';
12+
13+
/**
14+
* Override the default alias exist function. Provide as [Class, 'method'].
15+
*
16+
* @param array<string, string> $aliasExistCallback
17+
*/
18+
public function setAliasExistCallback(?array $aliasExistCallback): AliasFieldConfiguration
19+
{
20+
$this->aliasExistCallback = $aliasExistCallback;
21+
return $this;
22+
}
23+
24+
public function setFieldName(string $fieldName): AliasFieldConfiguration
25+
{
26+
$this->fieldName = $fieldName;
27+
return $this;
28+
}
29+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?php
2+
3+
namespace HeimrichHannot\UtilsBundle\EventListener\DcaField;
4+
5+
use Contao\CoreBundle\Framework\ContaoFramework;
6+
use Contao\CoreBundle\ServiceAnnotation\Hook;
7+
use Contao\CoreBundle\Slug\Slug;
8+
use Contao\Database;
9+
use Contao\DataContainer;
10+
use HeimrichHannot\UtilsBundle\Dca\AliasField;
11+
use HeimrichHannot\UtilsBundle\Dca\AliasFieldConfiguration;
12+
13+
class AliasDcaFieldListener extends AbstractDcaFieldListener
14+
{
15+
/**
16+
* @Hook("loadDataContainer")
17+
*/
18+
public function onLoadDataContainer(string $table): void
19+
{
20+
if (!isset(AliasField::getRegistrations()[$table])) {
21+
return;
22+
}
23+
24+
/** @var AliasFieldConfiguration $registration */
25+
$registration = AliasField::getRegistrations()[$table];
26+
27+
$field = AliasField::getField();
28+
if (is_array($registration->aliasExistCallback)) {
29+
$field['save_callback'][] = $registration->aliasExistCallback;
30+
}
31+
32+
$this->applyDefaultFieldAdjustments($field, $registration);
33+
34+
$GLOBALS['TL_DCA'][$table]['fields'][$registration->fieldName] = $field;
35+
}
36+
37+
public function onFieldsAliasSaveCallback($value, DataContainer $dc)
38+
{
39+
$framework = $this->container->get('contao.framework');
40+
$aliasExists = static function (string $alias) use ($dc, $framework): bool {
41+
return $framework->createInstance(Database::class)
42+
->prepare("SELECT id FROM $dc->table WHERE alias=? AND id!=?")
43+
->execute($alias, $dc->id)
44+
->numRows > 0;
45+
};
46+
47+
// Generate an alias if there is none
48+
if (!$value) {
49+
$value = $this->container->get('contao.slug')->generate(
50+
(string)$dc->activeRecord->title,
51+
(int)$dc->activeRecord->pid,
52+
$aliasExists
53+
);
54+
} elseif (preg_match('/^[1-9]\d*$/', $value)) {
55+
throw new \Exception(sprintf($GLOBALS['TL_LANG']['ERR']['aliasNumeric'], $value));
56+
} elseif ($aliasExists($value)) {
57+
throw new \Exception(sprintf($GLOBALS['TL_LANG']['ERR']['aliasExists'], $value));
58+
}
59+
60+
return $value;
61+
}
62+
63+
public static function getSubscribedServices(): array
64+
{
65+
return array_merge(
66+
[
67+
'contao.slug' => Slug::class,
68+
'contao.framework' => ContaoFramework::class,
69+
],
70+
parent::getSubscribedServices()
71+
);
72+
}#
73+
74+
75+
}
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
<?php
2+
3+
namespace EventListener\DcaField;
4+
5+
use Contao\CoreBundle\Framework\ContaoFramework;
6+
use Contao\CoreBundle\Slug\Slug;
7+
use Contao\Database;
8+
use Contao\DataContainer;
9+
use Dflydev\DotAccessData\Data;
10+
use HeimrichHannot\UtilsBundle\Dca\AliasField;
11+
use HeimrichHannot\UtilsBundle\EventListener\DcaField\AliasDcaFieldListener;
12+
use HeimrichHannot\UtilsBundle\Tests\AbstractUtilsTestCase;
13+
use PHPUnit\Framework\MockObject\MockBuilder;
14+
use Psr\Container\ContainerInterface;
15+
use function Clue\StreamFilter\fun;
16+
17+
class AliasDcaFieldListenerTest extends AbstractUtilsTestCase
18+
{
19+
20+
public function getTestInstance(array $parameters = [], ?MockBuilder $mockBuilder = null)
21+
{
22+
$container = $parameters['container'] ?? $this->createMock(ContainerInterface::class);
23+
24+
return new AliasDcaFieldListener($container);
25+
}
26+
27+
public function testOnLoadDataContainer()
28+
{
29+
$GLOBALS['TL_DCA']['tl_test'] = [];
30+
31+
$instance = $this->getTestInstance();
32+
$instance->onLoadDataContainer('tl_test');
33+
$this->assertEmpty($GLOBALS['TL_DCA']['tl_test']);
34+
35+
AliasField::register('tl_test');
36+
$instance->onLoadDataContainer('tl_test');
37+
$this->assertArrayHasKey('fields', $GLOBALS['TL_DCA']['tl_test']);
38+
$this->assertArrayHasKey('alias', $GLOBALS['TL_DCA']['tl_test']['fields']);
39+
$this->assertSame(
40+
[AliasDcaFieldListener::class, 'onFieldsAliasSaveCallback'],
41+
$GLOBALS['TL_DCA']['tl_test']['fields']['alias']['save_callback'][0]
42+
);
43+
44+
AliasField::register('tl_test')->setAliasExistCallback(null);
45+
$instance->onLoadDataContainer('tl_test');
46+
$this->assertArrayHasKey('fields', $GLOBALS['TL_DCA']['tl_test']);
47+
$this->assertArrayHasKey('alias', $GLOBALS['TL_DCA']['tl_test']['fields']);
48+
$this->assertEmpty(
49+
$GLOBALS['TL_DCA']['tl_test']['fields']['alias']['save_callback']
50+
);
51+
}
52+
53+
public function testOnFieldsAliasSaveCallbackGeneratesAliasIfEmpty()
54+
{
55+
$slug = $this->createMock(Slug::class);
56+
$slug->expects($this->once())
57+
->method('generate')
58+
->willReturn('generated-alias');
59+
60+
$framework = $this->createMock(ContaoFramework::class);
61+
62+
$container = $this->createMock(ContainerInterface::class);
63+
$container->method('get')->willReturnCallback(function (string $id) use ($slug, $framework) {
64+
switch ($id) {
65+
case 'contao.slug':
66+
case Slug::class:
67+
return $slug;
68+
case 'contao.framework':
69+
return $framework;
70+
default:
71+
throw new \InvalidArgumentException("Unknown service: $id");
72+
}
73+
});
74+
75+
$listener = $this->getTestInstance([
76+
'container' => $container,
77+
]);
78+
79+
$dc = new class () extends DataContainer
80+
{
81+
public int $id;
82+
public string $table;
83+
public object $activeRecord;
84+
85+
public function __construct()
86+
{
87+
}
88+
89+
public function __get($strKey)
90+
{
91+
if (isset($this->{$strKey})) {
92+
return $this->{$strKey};
93+
}
94+
95+
return parent::__get($strKey);
96+
}
97+
98+
public function __set($strKey, $varValue)
99+
{
100+
if (isset($this->{$strKey})) {
101+
$this->{$strKey} = $varValue;
102+
} else {
103+
parent::__set($strKey, $varValue);
104+
}
105+
}
106+
107+
public function getPalette()
108+
{
109+
// TODO: Implement getPalette() method.
110+
}
111+
112+
protected function save($varValue)
113+
{
114+
// TODO: Implement save() method.
115+
}
116+
};
117+
118+
// $dc = $this->createMock(DataContainer::class);
119+
$dc->activeRecord = (object)['title' => 'Test', 'pid' => 1];
120+
$dc->table = 'tl_article';
121+
$dc->id = 1;
122+
123+
$result = $listener->onFieldsAliasSaveCallback('', $dc);
124+
$this->assertEquals('generated-alias', $result);
125+
}
126+
127+
public function testOnFieldsAliasSaveCallbackThrowsOnNumericAlias()
128+
{
129+
$this->expectException(\Exception::class);
130+
131+
$slug = $this->createMock(Slug::class);
132+
$framework = $this->mockContaoFramework();
133+
$framework->method('createInstance')->willReturn($this->createMock(Database::class));
134+
135+
$container = $this->createMock(ContainerInterface::class);
136+
$container->method('get')->willReturnCallback(function (string $id) use ($slug, $framework) {
137+
switch ($id) {
138+
case 'contao.slug':
139+
case Slug::class:
140+
return $slug;
141+
case 'contao.framework':
142+
return $framework;
143+
default:
144+
throw new \InvalidArgumentException("Unknown service: $id");
145+
}
146+
});
147+
148+
149+
150+
$listener = $this->getTestInstance([
151+
'container' => $container,
152+
]);
153+
154+
$dc = $this->createMock(DataContainer::class);
155+
$dc->activeRecord = (object)['title' => 'Test', 'pid' => 1];
156+
$dc->table = 'tl_article';
157+
$dc->id = 1;
158+
159+
$GLOBALS['TL_LANG']['ERR']['aliasNumeric'] = 'Alias darf nicht numerisch sein: %s';
160+
161+
$listener->onFieldsAliasSaveCallback('123', $dc);
162+
}
163+
164+
public function testOnFieldsAliasSaveCallbackThrowsOnExistingAlias()
165+
{
166+
$this->expectException(\Exception::class);
167+
168+
$slug = $this->createMock(Slug::class);
169+
170+
$dbResult = new \stdClass();
171+
$dbResult->numRows = 1;
172+
173+
$db = $this->getMockBuilder(Database::class)
174+
->disableOriginalConstructor()
175+
->onlyMethods(['prepare', 'execute'])
176+
->getMock();
177+
$db->method('prepare')->willReturnSelf();
178+
$db->method('execute')->willReturn($dbResult);
179+
180+
$framework = $this->mockContaoFramework();
181+
$framework->method('createInstance')->willReturn($db);
182+
183+
$container = $this->createMock(ContainerInterface::class);
184+
$container->method('get')->willReturnCallback(function (string $id) use ($slug, $framework) {
185+
switch ($id) {
186+
case 'contao.slug':
187+
case Slug::class:
188+
return $slug;
189+
case 'contao.framework':
190+
return $framework;
191+
default:
192+
throw new \InvalidArgumentException("Unknown service: $id");
193+
}
194+
});
195+
196+
$listener = $this->getTestInstance([
197+
'container' => $container,
198+
]);
199+
200+
$dc = $this->createMock(DataContainer::class);
201+
$dc->activeRecord = (object)['title' => 'Test', 'pid' => 1];
202+
$dc->table = 'tl_article';
203+
$dc->id = 1;
204+
205+
$GLOBALS['TL_LANG']['ERR']['aliasExists'] = 'Alias existiert bereits: %s';
206+
207+
$listener->onFieldsAliasSaveCallback('existing-alias', $dc);
208+
}
209+
210+
}

0 commit comments

Comments
 (0)