Skip to content

Commit d750c73

Browse files
committed
DynamicListAttrManager
1 parent 1dbb679 commit d750c73

24 files changed

+956
-43
lines changed

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,8 @@
7777
"symfony/lock": "^6.4",
7878
"webklex/php-imap": "^6.2",
7979
"ext-imap": "*",
80-
"tatevikgr/rss-feed": "dev-main"
80+
"tatevikgr/rss-feed": "dev-main",
81+
"ext-pdo": "*"
8182
},
8283
"require-dev": {
8384
"phpunit/phpunit": "^9.5",

config/parameters.yml.dist

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ parameters:
2323
env(PHPLIST_DATABASE_PASSWORD): 'phplist'
2424
database_prefix: '%%env(DATABASE_PREFIX)%%'
2525
env(DATABASE_PREFIX): 'phplist_'
26+
list_table_prefix: '%%env(LIST_TABLE_PREFIX)%%'
27+
env(LIST_TABLE_PREFIX): 'listattr_'
2628

2729
# Email configuration
2830
app.mailer_from: '%%env(MAILER_FROM)%%'

config/services/managers.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,21 @@ services:
4848
autowire: true
4949
autoconfigure: true
5050

51+
PhpList\Core\Domain\Subscription\Service\Manager\DynamicListAttrManager:
52+
autowire: true
53+
autoconfigure: true
54+
arguments:
55+
$dbPrefix: '%database_prefix%'
56+
$dynamicListTablePrefix: '%list_table_prefix%'
57+
58+
PhpList\Core\Domain\Subscription\Service\Manager\DynamicListAttrTablesManager:
59+
autowire: true
60+
autoconfigure: true
61+
public: true
62+
arguments:
63+
$dbPrefix: '%database_prefix%'
64+
$dynamicListTablePrefix: '%list_table_prefix%'
65+
5166
PhpList\Core\Domain\Subscription\Service\Manager\SubscriberHistoryManager:
5267
autowire: true
5368
autoconfigure: true

config/services/repositories.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,8 @@ services:
7070
PhpList\Core\Domain\Subscription\Repository\DynamicListAttrRepository:
7171
autowire: true
7272
arguments:
73-
$prefix: '%database_prefix%'
73+
$dbPrefix: '%database_prefix%'
74+
$dynamicListTablePrefix: '%list_table_prefix%'
7475
PhpList\Core\Domain\Subscription\Repository\SubscriberHistoryRepository:
7576
parent: PhpList\Core\Domain\Common\Repository\AbstractRepository
7677
arguments:
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\Core\Domain\Subscription\Exception;
6+
7+
use RuntimeException;
8+
9+
class AttributeTypeChangeNotAllowed extends RuntimeException
10+
{
11+
public function __construct(string $oldType, string $newType)
12+
{
13+
parent::__construct(sprintf(
14+
'attribute_definition.type_change_not_allowed:%s->%s',
15+
$oldType,
16+
$newType
17+
));
18+
}
19+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\Core\Domain\Subscription\Model;
6+
7+
use PhpList\Core\Domain\Subscription\Exception\AttributeTypeChangeNotAllowed;
8+
9+
enum AttributeTypeEnum: string
10+
{
11+
case Text = 'text';
12+
case Number = 'number';
13+
case Date = 'date';
14+
case Select = 'select';
15+
case Checkbox = 'checkbox';
16+
case MultiSelect = 'multiselect';
17+
case CheckboxGroup = 'checkboxgroup';
18+
case Radio = 'radio';
19+
20+
public function equals(self $other): bool
21+
{
22+
return $this === $other;
23+
}
24+
25+
public function isMultiValued(): bool
26+
{
27+
return match ($this) {
28+
self::Select,
29+
self::Checkbox,
30+
self::MultiSelect,
31+
self::Radio,
32+
self::CheckboxGroup => true,
33+
default => false,
34+
};
35+
}
36+
37+
public function canTransitionTo(self $toType): bool
38+
{
39+
if ($this === $toType) {
40+
return true;
41+
}
42+
43+
if ($this->isMultiValued() !== $toType->isMultiValued()) {
44+
return false;
45+
}
46+
47+
return true;
48+
}
49+
50+
public function assertTransitionAllowed(self $toType): void
51+
{
52+
if (!$this->canTransitionTo($toType)) {
53+
throw new AttributeTypeChangeNotAllowed($this->value, $toType->value);
54+
}
55+
}
56+
}

src/Domain/Subscription/Model/Dto/AttributeDefinitionDto.php

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,26 @@
44

55
namespace PhpList\Core\Domain\Subscription\Model\Dto;
66

7+
use InvalidArgumentException;
8+
use PhpList\Core\Domain\Subscription\Model\AttributeTypeEnum;
9+
710
class AttributeDefinitionDto
811
{
912
/**
1013
* @SuppressWarnings("BooleanArgumentFlag")
14+
* @param DynamicListAttrDto[] $options
15+
* @throws InvalidArgumentException
1116
*/
1217
public function __construct(
1318
public readonly string $name,
14-
public readonly ?string $type = null,
19+
public readonly ?AttributeTypeEnum $type = null,
1520
public readonly ?int $listOrder = null,
1621
public readonly ?string $defaultValue = null,
1722
public readonly ?bool $required = false,
18-
public readonly ?string $tableName = null,
23+
public readonly array $options = [],
1924
) {
25+
if (trim($this->name) === '') {
26+
throw new InvalidArgumentException('Name cannot be empty');
27+
}
2028
}
2129
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\Core\Domain\Subscription\Model\Dto;
6+
7+
use InvalidArgumentException;
8+
use Symfony\Component\Serializer\Annotation\SerializedName;
9+
10+
class DynamicListAttrDto
11+
{
12+
public readonly ?int $id;
13+
14+
public readonly string $name;
15+
16+
#[SerializedName('listorder')]
17+
public readonly ?int $listOrder;
18+
19+
public function __construct(
20+
?int $id,
21+
string $name,
22+
?int $listOrder = null
23+
) {
24+
$trimmed = trim($name);
25+
if ($trimmed === '') {
26+
throw new InvalidArgumentException('Option name cannot be empty');
27+
}
28+
29+
$this->id = $id;
30+
$this->name = $trimmed;
31+
$this->listOrder = $listOrder;
32+
}
33+
}

src/Domain/Subscription/Model/SubscriberAttributeDefinition.php

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,10 @@ public function getName(): string
4848
return $this->name;
4949
}
5050

51-
public function getType(): ?string
51+
/** @SuppressWarnings(PHPMD.StaticAccess) */
52+
public function getType(): ?AttributeTypeEnum
5253
{
53-
return $this->type;
54+
return $this->type === null ? null : AttributeTypeEnum::from($this->type);
5455
}
5556

5657
public function getListOrder(): ?int
@@ -80,9 +81,19 @@ public function setName(string $name): self
8081
return $this;
8182
}
8283

83-
public function setType(?string $type): self
84+
/** @SuppressWarnings(PHPMD.StaticAccess) */
85+
public function setType(?AttributeTypeEnum $type): self
8486
{
85-
$this->type = $type;
87+
if ($type === null) {
88+
return $this;
89+
}
90+
91+
if ($this->type !== null) {
92+
$currentType = AttributeTypeEnum::from($this->type);
93+
$currentType->assertTransitionAllowed($type);
94+
}
95+
$this->type = $type->value;
96+
8697
return $this;
8798
}
8899

src/Domain/Subscription/Repository/DynamicListAttrRepository.php

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,37 @@
66

77
use Doctrine\DBAL\ArrayParameterType;
88
use Doctrine\DBAL\Connection;
9+
use Doctrine\DBAL\Exception;
910
use InvalidArgumentException;
11+
use PDO;
12+
use PhpList\Core\Domain\Subscription\Model\Dto\DynamicListAttrDto;
13+
use Symfony\Component\Serializer\Exception\ExceptionInterface;
14+
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
1015

1116
class DynamicListAttrRepository
1217
{
18+
private string $fullTablePrefix;
19+
1320
public function __construct(
1421
private readonly Connection $connection,
15-
private readonly string $prefix = 'phplist_'
22+
private readonly DenormalizerInterface $serializer,
23+
string $dbPrefix = 'phplist_',
24+
string $dynamicListTablePrefix = 'listattr_',
1625
) {
26+
$this->fullTablePrefix = $dbPrefix . $dynamicListTablePrefix;
27+
}
28+
29+
/**
30+
* @param array<int, array<string, mixed>> $rows
31+
* @return DynamicListAttrDto[]
32+
* @throws ExceptionInterface
33+
*/
34+
private function denormalizeAll(array $rows): array
35+
{
36+
return array_map(
37+
fn(array $row) => $this->serializer->denormalize($row, DynamicListAttrDto::class),
38+
$rows
39+
);
1740
}
1841

1942
/**
@@ -30,7 +53,7 @@ public function fetchOptionNames(string $listTable, array $ids): array
3053
throw new InvalidArgumentException('Invalid list table');
3154
}
3255

33-
$table = $this->prefix . 'listattr_' . $listTable;
56+
$table = $this->fullTablePrefix . $listTable;
3457

3558
$queryBuilder = $this->connection->createQueryBuilder();
3659
$queryBuilder->select('name')
@@ -47,7 +70,7 @@ public function fetchSingleOptionName(string $listTable, int $id): ?string
4770
throw new InvalidArgumentException('Invalid list table');
4871
}
4972

50-
$table = $this->prefix . 'listattr_' . $listTable;
73+
$table = $this->fullTablePrefix . $listTable;
5174

5275
$queryBuilder = $this->connection->createQueryBuilder();
5376
$queryBuilder->select('name')
@@ -59,4 +82,39 @@ public function fetchSingleOptionName(string $listTable, int $id): ?string
5982

6083
return $val === false ? null : (string) $val;
6184
}
85+
86+
public function existsByName(string $listTable, string $name): bool
87+
{
88+
if (!preg_match('/^[A-Za-z0-9_]+$/', $listTable)) {
89+
throw new InvalidArgumentException('Invalid list table');
90+
}
91+
92+
$table = $this->fullTablePrefix . $listTable;
93+
94+
try {
95+
$sql = 'SELECT 1 FROM ' . $table . ' WHERE LOWER(name) = LOWER(?) LIMIT 1';
96+
$result = $this->connection->fetchOne($sql, [$name], [PDO::PARAM_STR]);
97+
98+
return $result !== false;
99+
} catch (Exception $e) {
100+
throw $e;
101+
}
102+
}
103+
104+
public function getAll(string $listTable): array
105+
{
106+
if (!preg_match('/^[A-Za-z0-9_]+$/', $listTable)) {
107+
throw new InvalidArgumentException('Invalid list table');
108+
}
109+
110+
$table = $this->fullTablePrefix . $listTable;
111+
112+
$rows = $this->connection->createQueryBuilder()
113+
->select('id', 'name', 'listorder')
114+
->from($table)
115+
->executeQuery()
116+
->fetchAllAssociative();
117+
118+
return $this->denormalizeAll($rows);
119+
}
62120
}

0 commit comments

Comments
 (0)