From 61d97360ac692a814a5d97f10926980535df6aa0 Mon Sep 17 00:00:00 2001 From: Tatevik Date: Thu, 13 Nov 2025 11:04:16 +0400 Subject: [PATCH 01/11] AttributeDefinitionRequest --- README.md | 2 +- composer.json | 2 +- ...ubscriberAttributeDefinitionController.php | 15 ++- .../OpenApi/SwaggerSchemasRequest.php | 29 +++-- .../CreateAttributeDefinitionRequest.php | 33 ------ .../Request/SubscribePageRequest.php | 2 +- .../SubscriberAttributeDefinitionRequest.php | 67 ++++++++++++ .../UpdateAttributeDefinitionRequest.php | 33 ------ .../AttributeDefinitionNormalizer.php | 3 +- ...riberAttributeDefinitionControllerTest.php | 2 - .../CreateAttributeDefinitionRequestTest.php | 49 --------- ...bscriberAttributeDefinitionRequestTest.php | 102 ++++++++++++++++++ .../UpdateAttributeDefinitionRequestTest.php | 49 --------- .../AttributeDefinitionNormalizerTest.php | 2 - 14 files changed, 202 insertions(+), 188 deletions(-) delete mode 100644 src/Subscription/Request/CreateAttributeDefinitionRequest.php create mode 100644 src/Subscription/Request/SubscriberAttributeDefinitionRequest.php delete mode 100644 src/Subscription/Request/UpdateAttributeDefinitionRequest.php delete mode 100644 tests/Unit/Subscription/Request/CreateAttributeDefinitionRequestTest.php create mode 100644 tests/Unit/Subscription/Request/SubscriberAttributeDefinitionRequestTest.php delete mode 100644 tests/Unit/Subscription/Request/UpdateAttributeDefinitionRequestTest.php diff --git a/README.md b/README.md index b7a24171..e6b706f8 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ which also has more detailed installation instructions in the README. ## API Documentation -Visit `/docs` endpoint to access the full interactive documentation for `phpList/rest-api`. +Visit `https://phplist.github.io/restapi-docs/` endpoint to access the full interactive documentation for `phpList/rest-api`. Look at the **"API Documentation with Swagger"** section in the [contribution guide](.github/CONTRIBUTING.md) for more information on API documenation. diff --git a/composer.json b/composer.json index c4c880f8..99e5fcad 100644 --- a/composer.json +++ b/composer.json @@ -42,7 +42,7 @@ }, "require": { "php": "^8.1", - "phplist/core": "dev-main", + "phplist/core": "dev-dev", "friendsofsymfony/rest-bundle": "*", "symfony/test-pack": "^1.0", "symfony/process": "^6.4", diff --git a/src/Subscription/Controller/SubscriberAttributeDefinitionController.php b/src/Subscription/Controller/SubscriberAttributeDefinitionController.php index e096552e..ff3fd96c 100644 --- a/src/Subscription/Controller/SubscriberAttributeDefinitionController.php +++ b/src/Subscription/Controller/SubscriberAttributeDefinitionController.php @@ -12,8 +12,7 @@ use PhpList\RestBundle\Common\Controller\BaseController; use PhpList\RestBundle\Common\Service\Provider\PaginatedDataProvider; use PhpList\RestBundle\Common\Validator\RequestValidator; -use PhpList\RestBundle\Subscription\Request\CreateAttributeDefinitionRequest; -use PhpList\RestBundle\Subscription\Request\UpdateAttributeDefinitionRequest; +use PhpList\RestBundle\Subscription\Request\SubscriberAttributeDefinitionRequest; use PhpList\RestBundle\Subscription\Serializer\AttributeDefinitionNormalizer; use Symfony\Bridge\Doctrine\Attribute\MapEntity; use Symfony\Component\HttpFoundation\JsonResponse; @@ -51,7 +50,7 @@ public function __construct( requestBody: new OA\RequestBody( description: 'Pass parameters to create subscriber attribute.', required: true, - content: new OA\JsonContent(ref: '#/components/schemas/CreateSubscriberAttributeDefinitionRequest') + content: new OA\JsonContent(ref: '#/components/schemas/SubscriberAttributeDefinitionRequest') ), tags: ['subscriber-attributes'], parameters: [ @@ -85,8 +84,8 @@ public function create(Request $request): JsonResponse { $this->requireAuthentication($request); - /** @var CreateAttributeDefinitionRequest $definitionRequest */ - $definitionRequest = $this->validator->validate($request, CreateAttributeDefinitionRequest::class); + /** @var SubscriberAttributeDefinitionRequest $definitionRequest */ + $definitionRequest = $this->validator->validate($request, SubscriberAttributeDefinitionRequest::class); $attributeDefinition = $this->definitionManager->create($definitionRequest->getDto()); $this->entityManager->flush(); @@ -104,7 +103,7 @@ public function create(Request $request): JsonResponse requestBody: new OA\RequestBody( description: 'Pass parameters to update subscriber attribute.', required: true, - content: new OA\JsonContent(ref: '#/components/schemas/CreateSubscriberAttributeDefinitionRequest') + content: new OA\JsonContent(ref: '#/components/schemas/SubscriberAttributeDefinitionRequest') ), tags: ['subscriber-attributes'], parameters: [ @@ -150,8 +149,8 @@ public function update( throw $this->createNotFoundException('Attribute definition not found.'); } - /** @var UpdateAttributeDefinitionRequest $definitionRequest */ - $definitionRequest = $this->validator->validate($request, UpdateAttributeDefinitionRequest::class); + /** @var SubscriberAttributeDefinitionRequest $definitionRequest */ + $definitionRequest = $this->validator->validate($request, SubscriberAttributeDefinitionRequest::class); $attributeDefinition = $this->definitionManager->update( attributeDefinition: $attributeDefinition, diff --git a/src/Subscription/OpenApi/SwaggerSchemasRequest.php b/src/Subscription/OpenApi/SwaggerSchemasRequest.php index ac7b12b7..2a35698e 100644 --- a/src/Subscription/OpenApi/SwaggerSchemasRequest.php +++ b/src/Subscription/OpenApi/SwaggerSchemasRequest.php @@ -18,17 +18,32 @@ type: 'object' )] #[OA\Schema( - schema: 'CreateSubscriberAttributeDefinitionRequest', + schema: 'SubscriberAttributeDefinitionRequest', required: ['name'], properties: [ - new OA\Property(property: 'name', type: 'string', format: 'string', example: 'Country'), - new OA\Property(property: 'type', type: 'string', example: 'checkbox'), - new OA\Property(property: 'order', type: 'number', example: 12), - new OA\Property(property: 'default_value', type: 'string', example: 'United States'), + new OA\Property(property: 'name', type: 'string', example: 'Country'), + new OA\Property(property: 'type', type: 'string', example: 'checkbox', nullable: true), + new OA\Property(property: 'order', type: 'integer', example: 12, nullable: true), + new OA\Property(property: 'default_value', type: 'string', example: 'United States', nullable: true), new OA\Property(property: 'required', type: 'boolean', example: true), - new OA\Property(property: 'table_name', type: 'string', example: 'list_attributes'), + new OA\Property( + property: 'options', + type: 'array', + items: new OA\Items(ref: '#/components/schemas/DynamicListAttr'), + nullable: true, + ), ], - type: 'object' + type: 'object', +)] +#[OA\Schema( + schema: 'DynamicListAttr', + required: ['name'], + properties: [ + new OA\Property(property: 'id', type: 'integer', example: 1, nullable: true), + new OA\Property(property: 'name', type: 'string', example: 'United States'), + new OA\Property(property: 'listorder', type: 'integer', example: 10, nullable: true), + ], + type: 'object', )] #[OA\Schema( schema: 'CreateSubscriberRequest', diff --git a/src/Subscription/Request/CreateAttributeDefinitionRequest.php b/src/Subscription/Request/CreateAttributeDefinitionRequest.php deleted file mode 100644 index 411b7cba..00000000 --- a/src/Subscription/Request/CreateAttributeDefinitionRequest.php +++ /dev/null @@ -1,33 +0,0 @@ -name, - $this->type, - $this->order, - $this->defaultValue, - $this->required, - $this->tableName, - ); - } -} diff --git a/src/Subscription/Request/SubscribePageRequest.php b/src/Subscription/Request/SubscribePageRequest.php index cf22d20c..16f3eee5 100644 --- a/src/Subscription/Request/SubscribePageRequest.php +++ b/src/Subscription/Request/SubscribePageRequest.php @@ -14,7 +14,7 @@ class SubscribePageRequest implements RequestInterface public string $title; #[Assert\Type(type: 'bool')] - public ?bool $active = false; + public bool $active = false; public function getDto(): SubscribePageRequest { diff --git a/src/Subscription/Request/SubscriberAttributeDefinitionRequest.php b/src/Subscription/Request/SubscriberAttributeDefinitionRequest.php new file mode 100644 index 00000000..d079d870 --- /dev/null +++ b/src/Subscription/Request/SubscriberAttributeDefinitionRequest.php @@ -0,0 +1,67 @@ + [ + new Assert\Type(['type' => DynamicListAttrDto::class]), + ], + ])] + public ?array $options = null; + + public function getDto(): AttributeDefinitionDto + { + $type = $this->type === null ? null : AttributeTypeEnum::from($this->type); + return new AttributeDefinitionDto( + name: $this->name, + type: $type, + listOrder: $this->order, + defaultValue: $this->defaultValue, + required: $this->required, + options: $this->options ?? [], + ); + } + + public function validateType(ExecutionContextInterface $context): void + { + if ($this->type === null) { + return; + } + + $validator = new AttributeTypeValidator(new IdentityTranslator()); + + try { + $validator->validate($this->type); + } catch (ValidatorException $e) { + $context->buildViolation($e->getMessage()) + ->atPath('type') + ->addViolation(); + } + } +} diff --git a/src/Subscription/Request/UpdateAttributeDefinitionRequest.php b/src/Subscription/Request/UpdateAttributeDefinitionRequest.php deleted file mode 100644 index ba4fa951..00000000 --- a/src/Subscription/Request/UpdateAttributeDefinitionRequest.php +++ /dev/null @@ -1,33 +0,0 @@ -name, - $this->type, - $this->order, - $this->defaultValue, - $this->required, - $this->tableName, - ); - } -} diff --git a/src/Subscription/Serializer/AttributeDefinitionNormalizer.php b/src/Subscription/Serializer/AttributeDefinitionNormalizer.php index 598bfad4..1692ca7a 100644 --- a/src/Subscription/Serializer/AttributeDefinitionNormalizer.php +++ b/src/Subscription/Serializer/AttributeDefinitionNormalizer.php @@ -21,11 +21,10 @@ public function normalize($object, string $format = null, array $context = []): return [ 'id' => $object->getId(), 'name' => $object->getName(), - 'type' => $object->getType(), + 'type' => $object->getType() ? $object->getType()->value : null, 'list_order' => $object->getListOrder(), 'default_value' => $object->getDefaultValue(), 'required' => $object->isRequired(), - 'table_name' => $object->getTableName(), ]; } diff --git a/tests/Integration/Subscription/Controller/SubscriberAttributeDefinitionControllerTest.php b/tests/Integration/Subscription/Controller/SubscriberAttributeDefinitionControllerTest.php index 0768c813..59cdba35 100644 --- a/tests/Integration/Subscription/Controller/SubscriberAttributeDefinitionControllerTest.php +++ b/tests/Integration/Subscription/Controller/SubscriberAttributeDefinitionControllerTest.php @@ -51,7 +51,6 @@ public function testCreateAttributeDefinition() 'order' => 12, 'default_value' => 'United States', 'required' => true, - 'table_name' => 'list_attributes', ]); $this->authenticatedJsonRequest('POST', '/api/v2/subscribers/attributes', [], [], [], $payload); @@ -76,7 +75,6 @@ public function testUpdateAttributeDefinition() 'order' => 10, 'default_value' => 'Canada', 'required' => false, - 'table_name' => 'list_attributes', ]); $this->authenticatedJsonRequest('PUT', '/api/v2/subscribers/attributes/1', [], [], [], $payload); diff --git a/tests/Unit/Subscription/Request/CreateAttributeDefinitionRequestTest.php b/tests/Unit/Subscription/Request/CreateAttributeDefinitionRequestTest.php deleted file mode 100644 index d55376b9..00000000 --- a/tests/Unit/Subscription/Request/CreateAttributeDefinitionRequestTest.php +++ /dev/null @@ -1,49 +0,0 @@ -name = 'Test Attribute'; - $request->type = 'text'; - $request->order = 5; - $request->defaultValue = 'default'; - $request->required = true; - $request->tableName = 'test_table'; - - $dto = $request->getDto(); - - $this->assertInstanceOf(AttributeDefinitionDto::class, $dto); - $this->assertEquals('Test Attribute', $dto->name); - $this->assertEquals('text', $dto->type); - $this->assertEquals(5, $dto->listOrder); - $this->assertEquals('default', $dto->defaultValue); - $this->assertTrue($dto->required); - $this->assertEquals('test_table', $dto->tableName); - } - - public function testGetDtoWithDefaultValues(): void - { - $request = new CreateAttributeDefinitionRequest(); - $request->name = 'Test Attribute'; - - $dto = $request->getDto(); - - $this->assertInstanceOf(AttributeDefinitionDto::class, $dto); - $this->assertEquals('Test Attribute', $dto->name); - $this->assertNull($dto->type); - $this->assertNull($dto->listOrder); - $this->assertNull($dto->defaultValue); - $this->assertFalse($dto->required); - $this->assertNull($dto->tableName); - } -} diff --git a/tests/Unit/Subscription/Request/SubscriberAttributeDefinitionRequestTest.php b/tests/Unit/Subscription/Request/SubscriberAttributeDefinitionRequestTest.php new file mode 100644 index 00000000..d96c0585 --- /dev/null +++ b/tests/Unit/Subscription/Request/SubscriberAttributeDefinitionRequestTest.php @@ -0,0 +1,102 @@ +name = 'Test Attribute'; + $request->type = 'textline'; + $request->order = 5; + $request->defaultValue = 'default'; + $request->required = true; + + $dto = $request->getDto(); + + $this->assertInstanceOf(AttributeDefinitionDto::class, $dto); + $this->assertEquals('Test Attribute', $dto->name); + $this->assertInstanceOf(AttributeTypeEnum::class, $dto->type); + $this->assertSame(AttributeTypeEnum::TextLine, $dto->type); + $this->assertEquals(5, $dto->listOrder); + $this->assertEquals('default', $dto->defaultValue); + $this->assertTrue($dto->required); + $this->assertIsArray($dto->options); + } + + public function testGetDtoWithDefaultValues(): void + { + $request = new SubscriberAttributeDefinitionRequest(); + $request->name = 'Test Attribute'; + + $dto = $request->getDto(); + + $this->assertInstanceOf(AttributeDefinitionDto::class, $dto); + $this->assertEquals('Test Attribute', $dto->name); + $this->assertNull($dto->type); + $this->assertNull($dto->listOrder); + $this->assertNull($dto->defaultValue); + $this->assertFalse($dto->required); + $this->assertIsArray($dto->options); + $this->assertSame([], $dto->options); + } + + public function testGetDtoWithOptions(): void + { + $request = new SubscriberAttributeDefinitionRequest(); + $request->name = 'With options'; + $request->type = 'select'; + $request->options = [ + new DynamicListAttrDto(null, 'Option A', 1), + new DynamicListAttrDto(5, 'Option B', 2), + ]; + + $dto = $request->getDto(); + + $this->assertInstanceOf(AttributeDefinitionDto::class, $dto); + $this->assertSame('With options', $dto->name); + $this->assertSame(AttributeTypeEnum::Select, $dto->type); + $this->assertCount(2, $dto->options); + $this->assertInstanceOf(DynamicListAttrDto::class, $dto->options[0]); + $this->assertInstanceOf(DynamicListAttrDto::class, $dto->options[1]); + $this->assertSame('Option A', $dto->options[0]->name); + $this->assertSame('Option B', $dto->options[1]->name); + } + + + public function testValidationFailsWhenOptionsContainNonDto(): void + { + $request = new SubscriberAttributeDefinitionRequest(); + $request->name = 'Mixed options'; + $request->options = ['foo']; + + $validator = Validation::createValidatorBuilder()->enableAnnotationMapping()->getValidator(); + $violations = $validator->validate($request); + + $this->assertGreaterThan(0, $violations->count()); + $this->assertStringStartsWith('options[', $violations->get(0)->getPropertyPath()); + } + + public function testValidationFailsOnInvalidType(): void + { + $request = new SubscriberAttributeDefinitionRequest(); + $request->name = 'Invalid type'; + $request->type = 'not-a-valid-type'; + + $validator = Validation::createValidatorBuilder()->enableAnnotationMapping()->getValidator(); + $violations = $validator->validate($request); + + $this->assertGreaterThan(0, $violations->count()); + $this->assertSame('type', $violations->get(0)->getPropertyPath()); + } +} diff --git a/tests/Unit/Subscription/Request/UpdateAttributeDefinitionRequestTest.php b/tests/Unit/Subscription/Request/UpdateAttributeDefinitionRequestTest.php deleted file mode 100644 index 512b5342..00000000 --- a/tests/Unit/Subscription/Request/UpdateAttributeDefinitionRequestTest.php +++ /dev/null @@ -1,49 +0,0 @@ -name = 'Test Attribute'; - $request->type = 'text'; - $request->order = 5; - $request->defaultValue = 'default'; - $request->required = true; - $request->tableName = 'test_table'; - - $dto = $request->getDto(); - - $this->assertInstanceOf(AttributeDefinitionDto::class, $dto); - $this->assertEquals('Test Attribute', $dto->name); - $this->assertEquals('text', $dto->type); - $this->assertEquals(5, $dto->listOrder); - $this->assertEquals('default', $dto->defaultValue); - $this->assertTrue($dto->required); - $this->assertEquals('test_table', $dto->tableName); - } - - public function testGetDtoWithDefaultValues(): void - { - $request = new UpdateAttributeDefinitionRequest(); - $request->name = 'Test Attribute'; - - $dto = $request->getDto(); - - $this->assertInstanceOf(AttributeDefinitionDto::class, $dto); - $this->assertEquals('Test Attribute', $dto->name); - $this->assertNull($dto->type); - $this->assertNull($dto->listOrder); - $this->assertNull($dto->defaultValue); - $this->assertFalse($dto->required); - $this->assertNull($dto->tableName); - } -} diff --git a/tests/Unit/Subscription/Serializer/AttributeDefinitionNormalizerTest.php b/tests/Unit/Subscription/Serializer/AttributeDefinitionNormalizerTest.php index c2b8dc0e..95008ed3 100644 --- a/tests/Unit/Subscription/Serializer/AttributeDefinitionNormalizerTest.php +++ b/tests/Unit/Subscription/Serializer/AttributeDefinitionNormalizerTest.php @@ -30,7 +30,6 @@ public function testNormalize(): void $definition->method('getListOrder')->willReturn(12); $definition->method('getDefaultValue')->willReturn('US'); $definition->method('isRequired')->willReturn(true); - $definition->method('getTableName')->willReturn('user_attribute'); $normalizer = new AttributeDefinitionNormalizer(); $result = $normalizer->normalize($definition); @@ -43,7 +42,6 @@ public function testNormalize(): void 'list_order' => 12, 'default_value' => 'US', 'required' => true, - 'table_name' => 'user_attribute', ], $result); } From f6b8e0d0b17bc538abc501f52d17b4e261fc2182 Mon Sep 17 00:00:00 2001 From: Tatevik Date: Mon, 17 Nov 2025 10:22:17 +0400 Subject: [PATCH 02/11] Fix --- config/services/validators.yml | 5 +++++ .../Request/CreateAttributeDefinitionRequest.php | 2 -- .../Request/UpdateAttributeDefinitionRequest.php | 2 -- .../AdminAttributeDefinitionControllerTest.php | 10 ++++------ .../Fixtures/AdminAttributeDefinitionFixture.php | 1 - .../Controller/SubscriberImportControllerTest.php | 2 +- .../Fixtures/SubscriberAttributeDefinitionFixture.php | 3 ++- .../Fixtures/SubscriberAttributeValueFixture.php | 3 ++- .../Request/CreateAttributeDefinitionRequestTest.php | 3 --- .../Request/UpdateAttributeDefinitionRequestTest.php | 3 --- .../Serializer/AttributeDefinitionNormalizerTest.php | 3 ++- 11 files changed, 16 insertions(+), 21 deletions(-) diff --git a/config/services/validators.yml b/config/services/validators.yml index 02a0b425..e7302414 100644 --- a/config/services/validators.yml +++ b/config/services/validators.yml @@ -36,3 +36,8 @@ services: autowire: true autoconfigure: true tags: [ 'validator.constraint_validator' ] + + PhpList\Core\Domain\Identity\Validator\AttributeTypeValidator: + autowire: true + autoconfigure: true + diff --git a/src/Identity/Request/CreateAttributeDefinitionRequest.php b/src/Identity/Request/CreateAttributeDefinitionRequest.php index 794d086b..1edc8b3e 100644 --- a/src/Identity/Request/CreateAttributeDefinitionRequest.php +++ b/src/Identity/Request/CreateAttributeDefinitionRequest.php @@ -17,7 +17,6 @@ class CreateAttributeDefinitionRequest implements RequestInterface public ?int $order = null; public ?string $defaultValue = null; public bool $required = false; - public ?string $tableName = null; public function getDto(): AdminAttributeDefinitionDto { @@ -27,7 +26,6 @@ public function getDto(): AdminAttributeDefinitionDto listOrder: $this->order, defaultValue: $this->defaultValue, required: $this->required, - tableName: $this->tableName, ); } } diff --git a/src/Identity/Request/UpdateAttributeDefinitionRequest.php b/src/Identity/Request/UpdateAttributeDefinitionRequest.php index 8f387392..ef94c866 100644 --- a/src/Identity/Request/UpdateAttributeDefinitionRequest.php +++ b/src/Identity/Request/UpdateAttributeDefinitionRequest.php @@ -17,7 +17,6 @@ class UpdateAttributeDefinitionRequest implements RequestInterface public ?int $order = null; public ?string $defaultValue = null; public bool $required = false; - public ?string $tableName = null; public function getDto(): AdminAttributeDefinitionDto { @@ -27,7 +26,6 @@ public function getDto(): AdminAttributeDefinitionDto listOrder: $this->order, defaultValue: $this->defaultValue, required: $this->required, - tableName: $this->tableName, ); } } diff --git a/tests/Integration/Identity/Controller/AdminAttributeDefinitionControllerTest.php b/tests/Integration/Identity/Controller/AdminAttributeDefinitionControllerTest.php index dc3245d2..c6ff0acd 100644 --- a/tests/Integration/Identity/Controller/AdminAttributeDefinitionControllerTest.php +++ b/tests/Integration/Identity/Controller/AdminAttributeDefinitionControllerTest.php @@ -31,21 +31,19 @@ public function testCreateAttributeDefinitionWithValidDataReturnsCreated(): void { $this->authenticatedJsonRequest('post', '/api/v2/administrators/attributes', [], [], [], json_encode([ 'name' => 'Test Attribute', - 'type' => 'textarea', + 'type' => 'textline', 'order' => 1, 'defaultValue' => 'default', 'required' => true, - 'tableName' => 'test_table', ])); $this->assertHttpCreated(); $data = $this->getDecodedJsonResponseContent(); self::assertSame('Test Attribute', $data['name']); - self::assertSame('textarea', $data['type']); + self::assertSame('textline', $data['type']); self::assertSame(1, $data['list_order']); self::assertSame('default', $data['default_value']); self::assertTrue($data['required']); - self::assertSame('test_table', $data['table_name']); } public function testUpdateAttributeDefinitionReturnsOk(): void @@ -55,14 +53,14 @@ public function testUpdateAttributeDefinitionReturnsOk(): void $this->authenticatedJsonRequest('put', '/api/v2/administrators/attributes/' . $id, [], [], [], json_encode([ 'name' => 'Updated Attribute', - 'type' => 'checkbox', + 'type' => 'hidden', 'required' => true, ])); $this->assertHttpOkay(); $data = $this->getDecodedJsonResponseContent(); self::assertSame('Updated Attribute', $data['name']); - self::assertSame('checkbox', $data['type']); + self::assertSame('hidden', $data['type']); self::assertTrue($data['required']); } diff --git a/tests/Integration/Identity/Fixtures/AdminAttributeDefinitionFixture.php b/tests/Integration/Identity/Fixtures/AdminAttributeDefinitionFixture.php index f03bc3f4..f351d8dc 100644 --- a/tests/Integration/Identity/Fixtures/AdminAttributeDefinitionFixture.php +++ b/tests/Integration/Identity/Fixtures/AdminAttributeDefinitionFixture.php @@ -42,7 +42,6 @@ public function load(ObjectManager $manager): void $definition->setListOrder((int)$row['list_order']); $definition->setDefaultValue($row['default_value']); $definition->setRequired((bool)$row['required']); - $definition->setTableName($row['table_name']); $manager->persist($definition); } while (true); diff --git a/tests/Integration/Subscription/Controller/SubscriberImportControllerTest.php b/tests/Integration/Subscription/Controller/SubscriberImportControllerTest.php index e428df88..29c71115 100644 --- a/tests/Integration/Subscription/Controller/SubscriberImportControllerTest.php +++ b/tests/Integration/Subscription/Controller/SubscriberImportControllerTest.php @@ -238,7 +238,7 @@ public function testImportSubscribersWithSkipInvalidEmails(): void self::assertArrayHasKey('errors', $responseContent); self::assertEquals(0, $responseContent['imported']); self::assertEquals(1, $responseContent['skipped']); - self::assertEquals([], $responseContent['errors']); + self::assertEquals(['__Invalid email: invalid-email'], $responseContent['errors']); $this->authenticatedJsonRequest( 'POST', diff --git a/tests/Integration/Subscription/Fixtures/SubscriberAttributeDefinitionFixture.php b/tests/Integration/Subscription/Fixtures/SubscriberAttributeDefinitionFixture.php index c3386b4b..3fbb79a2 100644 --- a/tests/Integration/Subscription/Fixtures/SubscriberAttributeDefinitionFixture.php +++ b/tests/Integration/Subscription/Fixtures/SubscriberAttributeDefinitionFixture.php @@ -7,6 +7,7 @@ use Doctrine\Bundle\FixturesBundle\Fixture; use Doctrine\Common\DataFixtures\FixtureInterface; use Doctrine\Persistence\ObjectManager; +use PhpList\Core\Domain\Common\Model\AttributeTypeEnum; use PhpList\Core\Domain\Subscription\Model\SubscriberAttributeDefinition; class SubscriberAttributeDefinitionFixture extends Fixture implements FixtureInterface @@ -15,7 +16,7 @@ public function load(ObjectManager $manager): void { $definition = new SubscriberAttributeDefinition(); $definition->setName('Country'); - $definition->setType('checkbox'); + $definition->setType(AttributeTypeEnum::Checkbox); $definition->setListOrder(1); $definition->setDefaultValue('US'); $definition->setRequired(true); diff --git a/tests/Integration/Subscription/Fixtures/SubscriberAttributeValueFixture.php b/tests/Integration/Subscription/Fixtures/SubscriberAttributeValueFixture.php index c373e5db..2219604a 100644 --- a/tests/Integration/Subscription/Fixtures/SubscriberAttributeValueFixture.php +++ b/tests/Integration/Subscription/Fixtures/SubscriberAttributeValueFixture.php @@ -7,6 +7,7 @@ use Doctrine\Bundle\FixturesBundle\Fixture; use Doctrine\Common\DataFixtures\FixtureInterface; use Doctrine\Persistence\ObjectManager; +use PhpList\Core\Domain\Common\Model\AttributeTypeEnum; use PhpList\Core\Domain\Subscription\Model\Subscriber; use PhpList\Core\Domain\Subscription\Model\SubscriberAttributeDefinition; use PhpList\Core\Domain\Subscription\Model\SubscriberAttributeValue; @@ -17,7 +18,7 @@ public function load(ObjectManager $manager): void { $definition = new SubscriberAttributeDefinition(); $definition->setName('Country'); - $definition->setType('checkbox'); + $definition->setType(AttributeTypeEnum::Checkbox); $definition->setListOrder(1); $definition->setDefaultValue('US'); $definition->setRequired(true); diff --git a/tests/Unit/Identity/Request/CreateAttributeDefinitionRequestTest.php b/tests/Unit/Identity/Request/CreateAttributeDefinitionRequestTest.php index 82469388..f5c5fd4a 100644 --- a/tests/Unit/Identity/Request/CreateAttributeDefinitionRequestTest.php +++ b/tests/Unit/Identity/Request/CreateAttributeDefinitionRequestTest.php @@ -18,7 +18,6 @@ public function testGetDtoReturnsCorrectDto(): void $request->order = 5; $request->defaultValue = 'default'; $request->required = true; - $request->tableName = 'test_table'; $dto = $request->getDto(); @@ -28,7 +27,6 @@ public function testGetDtoReturnsCorrectDto(): void $this->assertEquals(5, $dto->listOrder); $this->assertEquals('default', $dto->defaultValue); $this->assertTrue($dto->required); - $this->assertEquals('test_table', $dto->tableName); } public function testGetDtoWithDefaultValues(): void @@ -44,6 +42,5 @@ public function testGetDtoWithDefaultValues(): void $this->assertNull($dto->listOrder); $this->assertNull($dto->defaultValue); $this->assertFalse($dto->required); - $this->assertNull($dto->tableName); } } diff --git a/tests/Unit/Identity/Request/UpdateAttributeDefinitionRequestTest.php b/tests/Unit/Identity/Request/UpdateAttributeDefinitionRequestTest.php index 41ea27eb..bec78e61 100644 --- a/tests/Unit/Identity/Request/UpdateAttributeDefinitionRequestTest.php +++ b/tests/Unit/Identity/Request/UpdateAttributeDefinitionRequestTest.php @@ -18,7 +18,6 @@ public function testGetDtoReturnsCorrectDto(): void $request->order = 10; $request->defaultValue = 'updated_default'; $request->required = true; - $request->tableName = 'updated_table'; $dto = $request->getDto(); @@ -28,7 +27,6 @@ public function testGetDtoReturnsCorrectDto(): void $this->assertEquals(10, $dto->listOrder); $this->assertEquals('updated_default', $dto->defaultValue); $this->assertTrue($dto->required); - $this->assertEquals('updated_table', $dto->tableName); } public function testGetDtoWithDefaultValues(): void @@ -44,6 +42,5 @@ public function testGetDtoWithDefaultValues(): void $this->assertNull($dto->listOrder); $this->assertNull($dto->defaultValue); $this->assertFalse($dto->required); - $this->assertNull($dto->tableName); } } diff --git a/tests/Unit/Subscription/Serializer/AttributeDefinitionNormalizerTest.php b/tests/Unit/Subscription/Serializer/AttributeDefinitionNormalizerTest.php index 95008ed3..23dbb38d 100644 --- a/tests/Unit/Subscription/Serializer/AttributeDefinitionNormalizerTest.php +++ b/tests/Unit/Subscription/Serializer/AttributeDefinitionNormalizerTest.php @@ -4,6 +4,7 @@ namespace PhpList\RestBundle\Tests\Unit\Subscription\Serializer; +use PhpList\Core\Domain\Common\Model\AttributeTypeEnum; use PhpList\Core\Domain\Subscription\Model\SubscriberAttributeDefinition; use PhpList\RestBundle\Subscription\Serializer\AttributeDefinitionNormalizer; use PHPUnit\Framework\TestCase; @@ -26,7 +27,7 @@ public function testNormalize(): void $definition = $this->createMock(SubscriberAttributeDefinition::class); $definition->method('getId')->willReturn(1); $definition->method('getName')->willReturn('Country'); - $definition->method('getType')->willReturn('text'); + $definition->method('getType')->willReturn(AttributeTypeEnum::Text); $definition->method('getListOrder')->willReturn(12); $definition->method('getDefaultValue')->willReturn('US'); $definition->method('isRequired')->willReturn(true); From 3570df9cf7448f073ac0aa9cc3fb0d1ccd6a56d0 Mon Sep 17 00:00:00 2001 From: Tatevik Date: Mon, 17 Nov 2025 11:00:12 +0400 Subject: [PATCH 03/11] Fix swagger --- .../AdminAttributeDefinitionController.php | 15 +-- .../OpenApi/SwaggerSchemasRequest.php | 15 ++- .../OpenApi/SwaggerSchemasResponse.php | 3 +- ...hp => AdminAttributeDefinitionRequest.php} | 2 +- .../UpdateAttributeDefinitionRequest.php | 31 ----- .../OpenApi/SwaggerSchemasRequest.php | 122 ------------------ .../OpenApi/SwaggerSchemasResponse.php | 15 +++ .../Request/CreateSubscriberListRequest.php | 12 ++ .../Request/CreateSubscriberRequest.php | 11 ++ .../SubscriberAttributeDefinitionRequest.php | 45 +++++++ .../Request/SubscribersExportRequest.php | 65 ++++++++-- .../Request/UpdateSubscriberRequest.php | 14 ++ ...> AdminAttributeDefinitionRequestTest.php} | 8 +- .../UpdateAttributeDefinitionRequestTest.php | 46 ------- 14 files changed, 179 insertions(+), 225 deletions(-) rename src/Identity/Request/{CreateAttributeDefinitionRequest.php => AdminAttributeDefinitionRequest.php} (92%) delete mode 100644 src/Identity/Request/UpdateAttributeDefinitionRequest.php delete mode 100644 src/Subscription/OpenApi/SwaggerSchemasRequest.php rename tests/Unit/Identity/Request/{CreateAttributeDefinitionRequestTest.php => AdminAttributeDefinitionRequestTest.php} (83%) delete mode 100644 tests/Unit/Identity/Request/UpdateAttributeDefinitionRequestTest.php diff --git a/src/Identity/Controller/AdminAttributeDefinitionController.php b/src/Identity/Controller/AdminAttributeDefinitionController.php index ff4c8531..4bf38643 100644 --- a/src/Identity/Controller/AdminAttributeDefinitionController.php +++ b/src/Identity/Controller/AdminAttributeDefinitionController.php @@ -12,8 +12,7 @@ use PhpList\RestBundle\Common\Controller\BaseController; use PhpList\RestBundle\Common\Service\Provider\PaginatedDataProvider; use PhpList\RestBundle\Common\Validator\RequestValidator; -use PhpList\RestBundle\Identity\Request\CreateAttributeDefinitionRequest; -use PhpList\RestBundle\Identity\Request\UpdateAttributeDefinitionRequest; +use PhpList\RestBundle\Identity\Request\AdminAttributeDefinitionRequest; use PhpList\RestBundle\Identity\Serializer\AdminAttributeDefinitionNormalizer; use Symfony\Bridge\Doctrine\Attribute\MapEntity; use Symfony\Component\HttpFoundation\JsonResponse; @@ -51,7 +50,7 @@ public function __construct( requestBody: new OA\RequestBody( description: 'Pass parameters to create admin attribute.', required: true, - content: new OA\JsonContent(ref: '#/components/schemas/CreateAdminAttributeDefinitionRequest') + content: new OA\JsonContent(ref: '#/components/schemas/AdminAttributeDefinitionRequest') ), tags: ['admin-attributes'], parameters: [ @@ -87,8 +86,8 @@ public function create(Request $request): JsonResponse { $this->requireAuthentication($request); - /** @var CreateAttributeDefinitionRequest $definitionRequest */ - $definitionRequest = $this->validator->validate($request, CreateAttributeDefinitionRequest::class); + /** @var AdminAttributeDefinitionRequest $definitionRequest */ + $definitionRequest = $this->validator->validate($request, AdminAttributeDefinitionRequest::class); $attributeDefinition = $this->definitionManager->create($definitionRequest->getDto()); $this->entityManager->flush(); @@ -107,7 +106,7 @@ public function create(Request $request): JsonResponse requestBody: new OA\RequestBody( description: 'Pass parameters to update admin attribute.', required: true, - content: new OA\JsonContent(ref: '#/components/schemas/CreateAdminAttributeDefinitionRequest') + content: new OA\JsonContent(ref: '#/components/schemas/AdminAttributeDefinitionRequest') ), tags: ['admin-attributes'], parameters: [ @@ -153,8 +152,8 @@ public function update( throw $this->createNotFoundException('Attribute definition not found.'); } - /** @var UpdateAttributeDefinitionRequest $definitionRequest */ - $definitionRequest = $this->validator->validate($request, UpdateAttributeDefinitionRequest::class); + /** @var AdminAttributeDefinitionRequest $definitionRequest */ + $definitionRequest = $this->validator->validate($request, AdminAttributeDefinitionRequest::class); $attributeDefinition = $this->definitionManager->update( attributeDefinition: $attributeDefinition, diff --git a/src/Identity/OpenApi/SwaggerSchemasRequest.php b/src/Identity/OpenApi/SwaggerSchemasRequest.php index d0421ecc..3b32ded2 100644 --- a/src/Identity/OpenApi/SwaggerSchemasRequest.php +++ b/src/Identity/OpenApi/SwaggerSchemasRequest.php @@ -5,6 +5,7 @@ namespace PhpList\RestBundle\Identity\OpenApi; use OpenApi\Attributes as OA; +use PhpList\Core\Domain\Common\Model\AttributeTypeEnum; #[OA\Schema( schema: 'CreateAdministratorRequest', @@ -96,15 +97,23 @@ type: 'object' )] #[OA\Schema( - schema: 'CreateAdminAttributeDefinitionRequest', + schema: 'AdminAttributeDefinitionRequest', required: ['name'], properties: [ new OA\Property(property: 'name', type: 'string', format: 'string', example: 'Country'), - new OA\Property(property: 'type', type: 'string', example: 'checkbox'), + new OA\Property( + property: 'type', + type: 'string', + enum: [ + AttributeTypeEnum::TextLine, + AttributeTypeEnum::Hidden, + ], + example: 'hidden', + nullable: true + ), new OA\Property(property: 'order', type: 'number', example: 12), new OA\Property(property: 'default_value', type: 'string', example: 'United States'), new OA\Property(property: 'required', type: 'boolean', example: true), - new OA\Property(property: 'table_name', type: 'string', example: 'list_attributes'), ], type: 'object' )] diff --git a/src/Identity/OpenApi/SwaggerSchemasResponse.php b/src/Identity/OpenApi/SwaggerSchemasResponse.php index 52b8837e..2d83cff0 100644 --- a/src/Identity/OpenApi/SwaggerSchemasResponse.php +++ b/src/Identity/OpenApi/SwaggerSchemasResponse.php @@ -27,11 +27,10 @@ properties: [ new OA\Property(property: 'id', type: 'integer', example: 1), new OA\Property(property: 'name', type: 'string', example: 'Country'), - new OA\Property(property: 'type', type: 'string', example: 'select'), + new OA\Property(property: 'type', type: 'string', example: 'hidden'), new OA\Property(property: 'list_order', type: 'integer', example: 12), new OA\Property(property: 'default_value', type: 'string', example: 'United States'), new OA\Property(property: 'required', type: 'boolean', example: true), - new OA\Property(property: 'table_name', type: 'string', example: 'ukcounties'), ], type: 'object' )] diff --git a/src/Identity/Request/CreateAttributeDefinitionRequest.php b/src/Identity/Request/AdminAttributeDefinitionRequest.php similarity index 92% rename from src/Identity/Request/CreateAttributeDefinitionRequest.php rename to src/Identity/Request/AdminAttributeDefinitionRequest.php index 1edc8b3e..053e4954 100644 --- a/src/Identity/Request/CreateAttributeDefinitionRequest.php +++ b/src/Identity/Request/AdminAttributeDefinitionRequest.php @@ -8,7 +8,7 @@ use PhpList\RestBundle\Common\Request\RequestInterface; use Symfony\Component\Validator\Constraints as Assert; -class CreateAttributeDefinitionRequest implements RequestInterface +class AdminAttributeDefinitionRequest implements RequestInterface { #[Assert\NotBlank] public string $name; diff --git a/src/Identity/Request/UpdateAttributeDefinitionRequest.php b/src/Identity/Request/UpdateAttributeDefinitionRequest.php deleted file mode 100644 index ef94c866..00000000 --- a/src/Identity/Request/UpdateAttributeDefinitionRequest.php +++ /dev/null @@ -1,31 +0,0 @@ -name, - type: $this->type, - listOrder: $this->order, - defaultValue: $this->defaultValue, - required: $this->required, - ); - } -} diff --git a/src/Subscription/OpenApi/SwaggerSchemasRequest.php b/src/Subscription/OpenApi/SwaggerSchemasRequest.php deleted file mode 100644 index 2a35698e..00000000 --- a/src/Subscription/OpenApi/SwaggerSchemasRequest.php +++ /dev/null @@ -1,122 +0,0 @@ -resolveDates(); return new SubscriberFilter( - $this->listId ?? null, - $subscribedFrom, - $subscribedTo, - $signupFrom, - $signupTo, - $changedFrom, - $changedTo, - $this->columns + listId: $this->listId ?? null, + subscribedDateFrom: $subscribedFrom, + subscribedDateTo: $subscribedTo, + createdDateFrom: $signupFrom, + createdDateTo: $signupTo, + updatedDateFrom: $changedFrom, + updatedDateTo: $changedTo, + columns: $this->columns ); } } diff --git a/src/Subscription/Request/UpdateSubscriberRequest.php b/src/Subscription/Request/UpdateSubscriberRequest.php index 1bce0d7d..4628ccaa 100644 --- a/src/Subscription/Request/UpdateSubscriberRequest.php +++ b/src/Subscription/Request/UpdateSubscriberRequest.php @@ -4,12 +4,26 @@ namespace PhpList\RestBundle\Subscription\Request; +use OpenApi\Attributes as OA; use PhpList\Core\Domain\Subscription\Model\Dto\UpdateSubscriberDto; use PhpList\Core\Domain\Subscription\Model\Subscriber; use PhpList\RestBundle\Common\Request\RequestInterface; use PhpList\RestBundle\Identity\Validator\Constraint\UniqueEmail; use Symfony\Component\Validator\Constraints as Assert; +#[OA\Schema( + schema: 'UpdateSubscriberRequest', + required: ['email'], + properties: [ + new OA\Property(property: 'email', type: 'string', format: 'string', example: 'admin@example.com'), + new OA\Property(property: 'confirmed', type: 'boolean', example: false), + new OA\Property(property: 'blacklisted', type: 'boolean', example: false), + new OA\Property(property: 'html_email', type: 'boolean', example: false), + new OA\Property(property: 'disabled', type: 'boolean', example: false), + new OA\Property(property: 'additional_data', type: 'string', example: 'asdf'), + ], + type: 'object' +)] class UpdateSubscriberRequest implements RequestInterface { public int $subscriberId; diff --git a/tests/Unit/Identity/Request/CreateAttributeDefinitionRequestTest.php b/tests/Unit/Identity/Request/AdminAttributeDefinitionRequestTest.php similarity index 83% rename from tests/Unit/Identity/Request/CreateAttributeDefinitionRequestTest.php rename to tests/Unit/Identity/Request/AdminAttributeDefinitionRequestTest.php index f5c5fd4a..d8d4accd 100644 --- a/tests/Unit/Identity/Request/CreateAttributeDefinitionRequestTest.php +++ b/tests/Unit/Identity/Request/AdminAttributeDefinitionRequestTest.php @@ -5,14 +5,14 @@ namespace PhpList\RestBundle\Tests\Unit\Identity\Request; use PhpList\Core\Domain\Identity\Model\Dto\AdminAttributeDefinitionDto; -use PhpList\RestBundle\Identity\Request\CreateAttributeDefinitionRequest; +use PhpList\RestBundle\Identity\Request\AdminAttributeDefinitionRequest; use PHPUnit\Framework\TestCase; -class CreateAttributeDefinitionRequestTest extends TestCase +class AdminAttributeDefinitionRequestTest extends TestCase { public function testGetDtoReturnsCorrectDto(): void { - $request = new CreateAttributeDefinitionRequest(); + $request = new AdminAttributeDefinitionRequest(); $request->name = 'Test Attribute'; $request->type = 'text'; $request->order = 5; @@ -31,7 +31,7 @@ public function testGetDtoReturnsCorrectDto(): void public function testGetDtoWithDefaultValues(): void { - $request = new CreateAttributeDefinitionRequest(); + $request = new AdminAttributeDefinitionRequest(); $request->name = 'Test Attribute'; $dto = $request->getDto(); diff --git a/tests/Unit/Identity/Request/UpdateAttributeDefinitionRequestTest.php b/tests/Unit/Identity/Request/UpdateAttributeDefinitionRequestTest.php deleted file mode 100644 index bec78e61..00000000 --- a/tests/Unit/Identity/Request/UpdateAttributeDefinitionRequestTest.php +++ /dev/null @@ -1,46 +0,0 @@ -name = 'Updated Attribute'; - $request->type = 'checkbox'; - $request->order = 10; - $request->defaultValue = 'updated_default'; - $request->required = true; - - $dto = $request->getDto(); - - $this->assertInstanceOf(AdminAttributeDefinitionDto::class, $dto); - $this->assertEquals('Updated Attribute', $dto->name); - $this->assertEquals('checkbox', $dto->type); - $this->assertEquals(10, $dto->listOrder); - $this->assertEquals('updated_default', $dto->defaultValue); - $this->assertTrue($dto->required); - } - - public function testGetDtoWithDefaultValues(): void - { - $request = new UpdateAttributeDefinitionRequest(); - $request->name = 'Updated Attribute'; - - $dto = $request->getDto(); - - $this->assertInstanceOf(AdminAttributeDefinitionDto::class, $dto); - $this->assertEquals('Updated Attribute', $dto->name); - $this->assertNull($dto->type); - $this->assertNull($dto->listOrder); - $this->assertNull($dto->defaultValue); - $this->assertFalse($dto->required); - } -} From 36100ea1fc33e2a479485f09b397a1e5f7604d07 Mon Sep 17 00:00:00 2001 From: Tatevik Date: Mon, 17 Nov 2025 11:57:13 +0400 Subject: [PATCH 04/11] Fix AdminAttributeDefinitionRequest choice --- .../AdminAttributeDefinitionRequest.php | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/Identity/Request/AdminAttributeDefinitionRequest.php b/src/Identity/Request/AdminAttributeDefinitionRequest.php index 053e4954..e31cf2f0 100644 --- a/src/Identity/Request/AdminAttributeDefinitionRequest.php +++ b/src/Identity/Request/AdminAttributeDefinitionRequest.php @@ -5,17 +5,26 @@ namespace PhpList\RestBundle\Identity\Request; use PhpList\Core\Domain\Identity\Model\Dto\AdminAttributeDefinitionDto; +use PhpList\Core\Domain\Subscription\Validator\AttributeTypeValidator; use PhpList\RestBundle\Common\Request\RequestInterface; +use Symfony\Component\Translation\IdentityTranslator; use Symfony\Component\Validator\Constraints as Assert; +use Symfony\Component\Validator\Context\ExecutionContextInterface; +use Symfony\Component\Validator\Exception\ValidatorException; +#[Assert\Callback('validateType')] class AdminAttributeDefinitionRequest implements RequestInterface { #[Assert\NotBlank] public string $name; + #[Assert\Choice(choices: ['hidden', 'textline'], message: 'Invalid type. Allowed values: hidden, textline.')] public ?string $type = null; + public ?int $order = null; + public ?string $defaultValue = null; + public bool $required = false; public function getDto(): AdminAttributeDefinitionDto @@ -28,4 +37,21 @@ public function getDto(): AdminAttributeDefinitionDto required: $this->required, ); } + + public function validateType(ExecutionContextInterface $context): void + { + if ($this->type === null) { + return; + } + + $validator = new AttributeTypeValidator(new IdentityTranslator()); + + try { + $validator->validate($this->type); + } catch (ValidatorException $e) { + $context->buildViolation($e->getMessage()) + ->atPath('type') + ->addViolation(); + } + } } From 2537a92aa6f37a62258c77fdcb02e0cec7019679 Mon Sep 17 00:00:00 2001 From: Tatevik Date: Mon, 17 Nov 2025 12:26:50 +0400 Subject: [PATCH 05/11] Add options to SubscriberAttributeDefinition --- .../OpenApi/SwaggerSchemasResponse.php | 2 +- .../SubscriberAttributeDefinitionRequest.php | 2 +- .../AttributeDefinitionNormalizer.php | 16 ++++++ .../AttributeDefinitionNormalizerTest.php | 54 +++++++++++++++++++ 4 files changed, 72 insertions(+), 2 deletions(-) diff --git a/src/Subscription/OpenApi/SwaggerSchemasResponse.php b/src/Subscription/OpenApi/SwaggerSchemasResponse.php index 03cbc028..29ddca75 100644 --- a/src/Subscription/OpenApi/SwaggerSchemasResponse.php +++ b/src/Subscription/OpenApi/SwaggerSchemasResponse.php @@ -108,7 +108,7 @@ properties: [ new OA\Property(property: 'id', type: 'integer', example: 1, nullable: false), new OA\Property(property: 'name', type: 'string', example: 'United States'), - new OA\Property(property: 'listorder', type: 'integer', example: 1, nullable: false), + new OA\Property(property: 'list_order', type: 'integer', example: 1, nullable: false), ], type: 'object', )] diff --git a/src/Subscription/Request/SubscriberAttributeDefinitionRequest.php b/src/Subscription/Request/SubscriberAttributeDefinitionRequest.php index cac803d0..2edb08cc 100644 --- a/src/Subscription/Request/SubscriberAttributeDefinitionRequest.php +++ b/src/Subscription/Request/SubscriberAttributeDefinitionRequest.php @@ -55,7 +55,7 @@ enum: [ properties: [ new OA\Property(property: 'id', type: 'integer', example: 1, nullable: true), new OA\Property(property: 'name', type: 'string', example: 'United States'), - new OA\Property(property: 'listorder', type: 'integer', example: 10, nullable: true), + new OA\Property(property: 'list_order', type: 'integer', example: 10, nullable: true), ], type: 'object', )] diff --git a/src/Subscription/Serializer/AttributeDefinitionNormalizer.php b/src/Subscription/Serializer/AttributeDefinitionNormalizer.php index 1692ca7a..1b716934 100644 --- a/src/Subscription/Serializer/AttributeDefinitionNormalizer.php +++ b/src/Subscription/Serializer/AttributeDefinitionNormalizer.php @@ -4,6 +4,7 @@ namespace PhpList\RestBundle\Subscription\Serializer; +use PhpList\Core\Domain\Subscription\Model\Dto\DynamicListAttrDto; use PhpList\Core\Domain\Subscription\Model\SubscriberAttributeDefinition; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; @@ -18,6 +19,20 @@ public function normalize($object, string $format = null, array $context = []): return []; } + $options = $object->getOptions(); + if (!empty($options)) { + $options = array_map(function ($option) { + if ($option instanceof DynamicListAttrDto) { + return [ + 'id' => $option->id, + 'name' => $option->name, + 'list_order' => $option->listOrder, + ]; + } + return $option; + }, $options); + } + return [ 'id' => $object->getId(), 'name' => $object->getName(), @@ -25,6 +40,7 @@ public function normalize($object, string $format = null, array $context = []): 'list_order' => $object->getListOrder(), 'default_value' => $object->getDefaultValue(), 'required' => $object->isRequired(), + 'options' => $options, ]; } diff --git a/tests/Unit/Subscription/Serializer/AttributeDefinitionNormalizerTest.php b/tests/Unit/Subscription/Serializer/AttributeDefinitionNormalizerTest.php index 23dbb38d..ecb0c4ea 100644 --- a/tests/Unit/Subscription/Serializer/AttributeDefinitionNormalizerTest.php +++ b/tests/Unit/Subscription/Serializer/AttributeDefinitionNormalizerTest.php @@ -5,6 +5,7 @@ namespace PhpList\RestBundle\Tests\Unit\Subscription\Serializer; use PhpList\Core\Domain\Common\Model\AttributeTypeEnum; +use PhpList\Core\Domain\Subscription\Model\Dto\DynamicListAttrDto; use PhpList\Core\Domain\Subscription\Model\SubscriberAttributeDefinition; use PhpList\RestBundle\Subscription\Serializer\AttributeDefinitionNormalizer; use PHPUnit\Framework\TestCase; @@ -43,6 +44,7 @@ public function testNormalize(): void 'list_order' => 12, 'default_value' => 'US', 'required' => true, + 'options' => [], ], $result); } @@ -53,4 +55,56 @@ public function testNormalizeWithInvalidObjectReturnsEmptyArray(): void self::assertSame([], $result); } + + public function testNormalizeWithOptions(): void + { + $options = [ + new DynamicListAttrDto( + id: 10, + name: 'USA', + listOrder: 1 + ), + new DynamicListAttrDto( + id: 20, + name: 'Canada', + listOrder: 2 + ), + ]; + + $definition = $this->createMock(SubscriberAttributeDefinition::class); + $definition->method('getId')->willReturn(5); + $definition->method('getName')->willReturn('Country'); + $definition->method('getType')->willReturn(AttributeTypeEnum::Select); + $definition->method('getListOrder')->willReturn(3); + $definition->method('getDefaultValue')->willReturn(null); + $definition->method('isRequired')->willReturn(false); + $definition->method('getOptions')->willReturn($options); + + $normalizer = new AttributeDefinitionNormalizer(); + $result = $normalizer->normalize($definition); + + self::assertIsArray($result); + + self::assertSame([ + 'id' => 5, + 'name' => 'Country', + 'type' => 'select', + 'list_order' => 3, + 'default_value' => null, + 'required' => false, + 'options' => [ + [ + 'id' => 10, + 'name' => 'USA', + 'list_order' => 1, + ], + [ + 'id' => 20, + 'name' => 'Canada', + 'list_order' => 2, + ], + ], + ], $result); + } + } From 454abeddd6e7f5eb614914cee53ea20ae8737e49 Mon Sep 17 00:00:00 2001 From: Tatevik Date: Mon, 17 Nov 2025 13:45:03 +0400 Subject: [PATCH 06/11] Fix get method --- .../Controller/SubscriberAttributeValueController.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Subscription/Controller/SubscriberAttributeValueController.php b/src/Subscription/Controller/SubscriberAttributeValueController.php index 24b54209..433cb74c 100644 --- a/src/Subscription/Controller/SubscriberAttributeValueController.php +++ b/src/Subscription/Controller/SubscriberAttributeValueController.php @@ -351,8 +351,6 @@ public function getAttributeDefinition( throw $this->createNotFoundException('Subscriber attribute not found.'); } $attribute = $this->attributeManager->getSubscriberAttribute($subscriber->getId(), $definition->getId()); - $this->attributeManager->delete($attribute); - $this->entityManager->flush(); return $this->json( $this->normalizer->normalize($attribute), From bcff81033a2719f58a4aeb107ffdef49b11dbedb Mon Sep 17 00:00:00 2001 From: Tatevik Date: Mon, 17 Nov 2025 13:53:39 +0400 Subject: [PATCH 07/11] Fix style --- .../Serializer/AttributeDefinitionNormalizer.php | 13 +++++-------- .../AttributeDefinitionNormalizerTest.php | 4 +--- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/src/Subscription/Serializer/AttributeDefinitionNormalizer.php b/src/Subscription/Serializer/AttributeDefinitionNormalizer.php index 1b716934..73acfc59 100644 --- a/src/Subscription/Serializer/AttributeDefinitionNormalizer.php +++ b/src/Subscription/Serializer/AttributeDefinitionNormalizer.php @@ -22,14 +22,11 @@ public function normalize($object, string $format = null, array $context = []): $options = $object->getOptions(); if (!empty($options)) { $options = array_map(function ($option) { - if ($option instanceof DynamicListAttrDto) { - return [ - 'id' => $option->id, - 'name' => $option->name, - 'list_order' => $option->listOrder, - ]; - } - return $option; + return [ + 'id' => $option->id, + 'name' => $option->name, + 'list_order' => $option->listOrder, + ]; }, $options); } diff --git a/tests/Unit/Subscription/Serializer/AttributeDefinitionNormalizerTest.php b/tests/Unit/Subscription/Serializer/AttributeDefinitionNormalizerTest.php index ecb0c4ea..3821a8f0 100644 --- a/tests/Unit/Subscription/Serializer/AttributeDefinitionNormalizerTest.php +++ b/tests/Unit/Subscription/Serializer/AttributeDefinitionNormalizerTest.php @@ -97,8 +97,7 @@ public function testNormalizeWithOptions(): void 'id' => 10, 'name' => 'USA', 'list_order' => 1, - ], - [ + ], [ 'id' => 20, 'name' => 'Canada', 'list_order' => 2, @@ -106,5 +105,4 @@ public function testNormalizeWithOptions(): void ], ], $result); } - } From 4052d0975ce4c4e412151b77de4d0e0c422a8b30 Mon Sep 17 00:00:00 2001 From: Tatevik Date: Mon, 17 Nov 2025 14:00:20 +0400 Subject: [PATCH 08/11] Own namespace for attributes --- .../SubscriberAttributeDefinitionController.php | 12 ++++++------ ...SubscriberAttributeDefinitionControllerTest.php | 14 +++++++------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Subscription/Controller/SubscriberAttributeDefinitionController.php b/src/Subscription/Controller/SubscriberAttributeDefinitionController.php index ff3fd96c..e619b2ac 100644 --- a/src/Subscription/Controller/SubscriberAttributeDefinitionController.php +++ b/src/Subscription/Controller/SubscriberAttributeDefinitionController.php @@ -20,7 +20,7 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; -#[Route('/subscribers/attributes', name: 'subscriber_attribute_definition_')] +#[Route('/attributes', name: 'subscriber_attribute_definition_')] class SubscriberAttributeDefinitionController extends BaseController { private AttributeDefinitionManager $definitionManager; @@ -43,7 +43,7 @@ public function __construct( #[Route('', name: 'create', methods: ['POST'])] #[OA\Post( - path: '/api/v2/subscribers/attributes', + path: '/api/v2/attributes', description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . 'Returns created subscriber attribute definition.', summary: 'Create a subscriber attribute definition.', @@ -96,7 +96,7 @@ public function create(Request $request): JsonResponse #[Route('/{definitionId}', name: 'update', requirements: ['definitionId' => '\d+'], methods: ['PUT'])] #[OA\Put( - path: '/api/v2/subscribers/attributes/{definitionId}', + path: '/api/v2/attributes/{definitionId}', description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . 'Returns updated subscriber attribute definition.', summary: 'Update a subscriber attribute definition.', @@ -164,7 +164,7 @@ public function update( #[Route('/{definitionId}', name: 'delete', requirements: ['definitionId' => '\d+'], methods: ['DELETE'])] #[OA\Delete( - path: '/api/v2/subscribers/attributes/{definitionId}', + path: '/api/v2/attributes/{definitionId}', description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . 'Deletes a single subscriber attribute definition.', summary: 'Deletes an attribute definition.', @@ -219,7 +219,7 @@ public function delete( #[Route('', name: 'get_list', methods: ['GET'])] #[OA\Get( - path: '/api/v2/subscribers/attributes', + path: '/api/v2/attributes', description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . 'Returns a JSON list of all subscriber attribute definitions.', summary: 'Gets a list of all subscriber attribute definitions.', @@ -286,7 +286,7 @@ public function getPaginated(Request $request): JsonResponse #[Route('/{definitionId}', name: 'get_one', requirements: ['definitionId' => '\d+'], methods: ['GET'])] #[OA\Get( - path: '/api/v2/subscribers/attributes/{definitionId}', + path: '/api/v2/attributes/{definitionId}', description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . 'Returns a single attribute with specified ID.', summary: 'Gets attribute with specified ID.', diff --git a/tests/Integration/Subscription/Controller/SubscriberAttributeDefinitionControllerTest.php b/tests/Integration/Subscription/Controller/SubscriberAttributeDefinitionControllerTest.php index 59cdba35..5f8e8fa7 100644 --- a/tests/Integration/Subscription/Controller/SubscriberAttributeDefinitionControllerTest.php +++ b/tests/Integration/Subscription/Controller/SubscriberAttributeDefinitionControllerTest.php @@ -23,21 +23,21 @@ public function testControllerIsAvailableViaContainer() public function testGetAttributesWithoutSessionKeyReturnsForbidden() { - self::getClient()->request('GET', '/api/v2/subscribers/attributes'); + self::getClient()->request('GET', '/api/v2/attributes'); $this->assertHttpForbidden(); } public function testGetAttributesWithSessionKeyReturnsOk() { $this->loadFixtures([AdministratorFixture::class, AdministratorTokenFixture::class]); - $this->authenticatedJsonRequest('GET', '/api/v2/subscribers/attributes'); + $this->authenticatedJsonRequest('GET', '/api/v2/attributes'); $this->assertHttpOkay(); } public function testGetAttributeWithInvalidIdReturnsNotFound() { $this->loadFixtures([AdministratorFixture::class, AdministratorTokenFixture::class]); - $this->authenticatedJsonRequest('GET', '/api/v2/subscribers/attributes/999'); + $this->authenticatedJsonRequest('GET', '/api/v2/attributes/999'); $this->assertHttpNotFound(); } @@ -53,7 +53,7 @@ public function testCreateAttributeDefinition() 'required' => true, ]); - $this->authenticatedJsonRequest('POST', '/api/v2/subscribers/attributes', [], [], [], $payload); + $this->authenticatedJsonRequest('POST', '/api/v2/attributes', [], [], [], $payload); $this->assertHttpCreated(); @@ -77,7 +77,7 @@ public function testUpdateAttributeDefinition() 'required' => false, ]); - $this->authenticatedJsonRequest('PUT', '/api/v2/subscribers/attributes/1', [], [], [], $payload); + $this->authenticatedJsonRequest('PUT', '/api/v2/attributes/1', [], [], [], $payload); $this->assertHttpOkay(); $response = $this->getDecodedJsonResponseContent(); self::assertSame('Updated Country', $response['name']); @@ -91,7 +91,7 @@ public function testDeleteAttributeDefinition() SubscriberAttributeDefinitionFixture::class, ]); - $this->authenticatedJsonRequest('DELETE', '/api/v2/subscribers/attributes/1'); + $this->authenticatedJsonRequest('DELETE', '/api/v2/attributes/1'); $this->assertHttpNoContent(); $repo = self::getContainer()->get(SubscriberAttributeDefinitionRepository::class); @@ -108,7 +108,7 @@ public function testCreateAttributeDefinitionMissingNameReturnsValidationError() 'required' => false ]); - $this->authenticatedJsonRequest('POST', '/api/v2/subscribers/attributes', [], [], [], $payload); + $this->authenticatedJsonRequest('POST', '/api/v2/attributes', [], [], [], $payload); $this->assertHttpUnprocessableEntity(); } } From 470a02ce63db6c50f3dab46c4a4629c609a90b77 Mon Sep 17 00:00:00 2001 From: Tatevik Date: Mon, 17 Nov 2025 14:06:35 +0400 Subject: [PATCH 09/11] Hydrated attribute --- .../SubscriberAttributeDefinitionController.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Subscription/Controller/SubscriberAttributeDefinitionController.php b/src/Subscription/Controller/SubscriberAttributeDefinitionController.php index e619b2ac..251ac2f5 100644 --- a/src/Subscription/Controller/SubscriberAttributeDefinitionController.php +++ b/src/Subscription/Controller/SubscriberAttributeDefinitionController.php @@ -7,6 +7,7 @@ use Doctrine\ORM\EntityManagerInterface; use OpenApi\Attributes as OA; use PhpList\Core\Domain\Subscription\Model\SubscriberAttributeDefinition; +use PhpList\Core\Domain\Subscription\Repository\SubscriberAttributeDefinitionRepository; use PhpList\Core\Domain\Subscription\Service\Manager\AttributeDefinitionManager; use PhpList\Core\Security\Authentication; use PhpList\RestBundle\Common\Controller\BaseController; @@ -343,6 +344,13 @@ public function getAttributeDefinition( throw $this->createNotFoundException('Attribute definition not found.'); } + /** @var SubscriberAttributeDefinitionRepository $repo */ + $repo = $this->entityManager->getRepository(SubscriberAttributeDefinition::class); + $hydrated = $repo->findOneByName($attributeDefinition->getName()); + if ($hydrated instanceof SubscriberAttributeDefinition) { + $attributeDefinition = $hydrated; + } + return $this->json( $this->normalizer->normalize($attributeDefinition), Response::HTTP_OK From 014611d155a2fdfd2afc8eccbe00e9c8fb095780 Mon Sep 17 00:00:00 2001 From: Tatevik Date: Mon, 17 Nov 2025 14:13:19 +0400 Subject: [PATCH 10/11] Use core dev-main --- composer.json | 2 +- .../Request/SubscriberAttributeDefinitionRequest.php | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 99e5fcad..c4c880f8 100644 --- a/composer.json +++ b/composer.json @@ -42,7 +42,7 @@ }, "require": { "php": "^8.1", - "phplist/core": "dev-dev", + "phplist/core": "dev-main", "friendsofsymfony/rest-bundle": "*", "symfony/test-pack": "^1.0", "symfony/process": "^6.4", diff --git a/src/Subscription/Request/SubscriberAttributeDefinitionRequest.php b/src/Subscription/Request/SubscriberAttributeDefinitionRequest.php index 2edb08cc..7e4fa02b 100644 --- a/src/Subscription/Request/SubscriberAttributeDefinitionRequest.php +++ b/src/Subscription/Request/SubscriberAttributeDefinitionRequest.php @@ -82,7 +82,10 @@ class SubscriberAttributeDefinitionRequest implements RequestInterface public function getDto(): AttributeDefinitionDto { - $type = $this->type === null ? null : AttributeTypeEnum::from($this->type); + $type = null; + if ($this->type !== null) { + $type = AttributeTypeEnum::tryFrom($this->type); + } return new AttributeDefinitionDto( name: $this->name, type: $type, From 3fcdb9c612bebae4284820a07b7b2d6b5baff439 Mon Sep 17 00:00:00 2001 From: Tatevik Date: Mon, 17 Nov 2025 14:22:02 +0400 Subject: [PATCH 11/11] API responses --- src/Common/EventListener/ExceptionListener.php | 6 ++++++ .../Controller/SubscriberAttributeDefinitionController.php | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/src/Common/EventListener/ExceptionListener.php b/src/Common/EventListener/ExceptionListener.php index 6db218f1..b898defa 100644 --- a/src/Common/EventListener/ExceptionListener.php +++ b/src/Common/EventListener/ExceptionListener.php @@ -6,6 +6,7 @@ use Exception; use PhpList\Core\Domain\Identity\Exception\AdminAttributeCreationException; +use PhpList\Core\Domain\Subscription\Exception\AttributeDefinitionCreationException; use PhpList\Core\Domain\Subscription\Exception\SubscriptionCreationException; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpKernel\Event\ExceptionEvent; @@ -42,6 +43,11 @@ public function onKernelException(ExceptionEvent $event): void 'message' => $exception->getMessage(), ], $exception->getStatusCode()); $event->setResponse($response); + } elseif ($exception instanceof AttributeDefinitionCreationException) { + $response = new JsonResponse([ + 'message' => $exception->getMessage(), + ], $exception->getStatusCode()); + $event->setResponse($response); } elseif ($exception instanceof ValidatorException) { $response = new JsonResponse([ 'message' => $exception->getMessage(), diff --git a/src/Subscription/Controller/SubscriberAttributeDefinitionController.php b/src/Subscription/Controller/SubscriberAttributeDefinitionController.php index 251ac2f5..79c3b361 100644 --- a/src/Subscription/Controller/SubscriberAttributeDefinitionController.php +++ b/src/Subscription/Controller/SubscriberAttributeDefinitionController.php @@ -74,6 +74,11 @@ public function __construct( description: 'Failure', content: new OA\JsonContent(ref: '#/components/schemas/UnauthorizedResponse') ), + new OA\Response( + response: 409, + description: 'Failure', + content: new OA\JsonContent(ref: '#/components/schemas/AlreadyExistsResponse') + ), new OA\Response( response: 422, description: 'Failure',