Skip to content

Conversation

@TatevikGr
Copy link
Contributor

@TatevikGr TatevikGr commented Nov 17, 2025

Summary by CodeRabbit

Release Notes

  • Documentation

    • Updated API documentation reference to external hosted documentation.
  • New Features

    • Added support for attribute options in subscriber attribute definitions.
    • Enhanced attribute validation with improved error reporting.
  • Changes

    • Restructured API endpoint paths for subscriber attributes.
    • Removed deprecated table_name field from attribute definitions.
    • Attribute type field now restricted to predefined values.

Thanks for contributing to phpList!

@coderabbitai
Copy link

coderabbitai bot commented Nov 17, 2025

📝 Walkthrough

Walkthrough

This pull request consolidates attribute definition request handlers across the Identity and Subscription domains. It unifies separate create/update request classes into single unified request classes (AdminAttributeDefinitionRequest and SubscriberAttributeDefinitionRequest), removes the table_name property from attribute definitions, and migrates attribute type handling from strings to AttributeTypeEnum. Additionally, the subscriber attribute routes are moved from /subscribers/attributes to /attributes, and OpenAPI schemas are updated to reflect these changes. The refactoring includes corresponding updates to controllers, serializers, validators, and test files to maintain compatibility.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Controller
    participant Validator
    participant RequestDTO
    participant Repository
    participant Response

    Client->>Controller: POST /api/v2/attributes (JSON payload)
    Controller->>Validator: validate(request, SubscriberAttributeDefinitionRequest)
    Validator->>RequestDTO: validateType() callback
    RequestDTO->>Validator: AttributeTypeValidator.validate(type)
    alt Type is valid
        Validator-->>Controller: validation passes
        Controller->>RequestDTO: getDto() returns AttributeDefinitionDto
        Controller->>Repository: create/update attributeDefinition
        Repository-->>Controller: AttributeDefinition persisted
        Controller-->>Response: 201/204 response
        Response-->>Client: Success
    else Type is invalid
        Validator-->>Controller: validation error
        Controller-->>Response: 422 Unprocessable Entity
        Response-->>Client: Validation error with message
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~35 minutes

  • Route path changes in SubscriberAttributeDefinitionController — endpoints shifted from /subscribers/attributes to /attributes affecting all CRUD operations; verify backward compatibility expectations and test coverage
  • Enum type conversion across multiple filesAttributeTypeEnum replaces string types in fixtures, tests, and serializers; ensure all conversions are complete and handle nullability correctly
  • Request DTO consolidation — unification from separate create/update classes to single request classes; verify no logic loss in validation or DTO mapping, particularly with new validateType() callback mechanism
  • New AttributeDefinitionCreationException handling — verify placement in exception listener doesn't shadow other exception types and status codes are appropriate
  • OpenAPI schema updates — ensure DynamicListAttrOption integration with AttributeDefinition.options array is properly documented and tested

Possibly related PRs

  • Dev #159 — Touches the same controllers and request DTO patterns (AdminAttributeDefinitionController, SubscriberAttributeDefinitionController), suggesting related refactoring efforts in attribute definition management

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.70% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Refactor attribute definition request handling' accurately captures the main focus of this changeset, which consolidates separate request classes into unified ones and restructures request handling across Identity and Subscription domains.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dev

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/Subscription/OpenApi/SwaggerSchemasResponse.php (1)

88-105: AttributeDefinition schema is out of sync with the normalizer

Right now the AttributeDefinition schema still documents table_name, and now also an options array, but the AttributeDefinitionNormalizer no longer emits table_name and doesn’t include options at all.

That gives clients a misleading contract:

  • table_name appears in the OpenAPI spec but is never present in responses.
  • options is documented but won’t show up until serialization is updated.

I’d recommend:

  • Dropping the table_name property from this schema, and
  • Either adding options to AttributeDefinitionNormalizer or removing it from the schema until it’s actually returned.
🧹 Nitpick comments (3)
src/Subscription/Request/UpdateSubscriberRequest.php (1)

7-26: Align OpenAPI required vs PHP property nullability

The schema only requires email, but confirmed, blacklisted, html_email, disabled, and additional_data are mapped to non-nullable typed properties (bool/string) without defaults in UpdateSubscriberRequest. If clients follow the docs and omit these fields on update, you risk uninitialized typed properties at runtime.

Either:

  • mark these fields as required in the OA schema, or
  • make the PHP properties nullable / give safe defaults so partial updates are actually supported.
src/Identity/OpenApi/SwaggerSchemasRequest.php (1)

100-117: AdminAttributeDefinitionRequest schema looks good, with a small type nit

Nice reuse of AttributeTypeEnum for the type enum; that keeps docs and domain in sync.

One small consistency tweak: order is documented as type: 'number' here, while similar fields (order / list_order) are integers elsewhere. You might want to switch this to type: 'integer' for a crisper contract and to match the underlying PHP/int type.

src/Subscription/Request/SubscriberAttributeDefinitionRequest.php (1)

18-61: Solid request DTO overall; just double-check options hydration and enum conversion

This request class is nicely structured: OA schema, validation, and DTO mapping all line up with the AttributeTypeEnum-based design and the new options support.

A couple of things worth sanity-checking:

  • options element type vs hydration
    options is declared as ?array and validated with Assert\Type(['type' => DynamicListAttrDto::class]) for each item. That assumes the deserialization layer is already instantiating DynamicListAttrDto objects from the incoming JSON. If it actually leaves them as plain arrays, this constraint will always fail and $options will never validate. If there isn’t already a custom denormalizer in place, consider either:

    • mapping raw arrays to DynamicListAttrDto instances in getDto(), or
    • relaxing the constraint to validate array shape instead of object type.
  • AttributeTypeEnum::from() safety
    getDto() uses AttributeTypeEnum::from($this->type) which will throw on an unknown string. As long as every code path validates the request (and thus runs validateType()) before calling getDto(), that’s fine. If there’s any chance getDto() might be called on an unvalidated instance (e.g., in tests or internal helpers), you might prefer AttributeTypeEnum::tryFrom() plus a validation error, or reuse AttributeTypeValidator here to avoid hard exceptions.

Nothing blocking, but tightening these up would make this DTO a bit more robust.

Also applies to: 74-111

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f4c6993 and 3570df9.

📒 Files selected for processing (32)
  • README.md (1 hunks)
  • composer.json (1 hunks)
  • config/services/validators.yml (1 hunks)
  • src/Identity/Controller/AdminAttributeDefinitionController.php (5 hunks)
  • src/Identity/OpenApi/SwaggerSchemasRequest.php (2 hunks)
  • src/Identity/OpenApi/SwaggerSchemasResponse.php (1 hunks)
  • src/Identity/Request/AdminAttributeDefinitionRequest.php (1 hunks)
  • src/Identity/Request/UpdateAttributeDefinitionRequest.php (0 hunks)
  • src/Subscription/Controller/SubscriberAttributeDefinitionController.php (5 hunks)
  • src/Subscription/OpenApi/SwaggerSchemasRequest.php (0 hunks)
  • src/Subscription/OpenApi/SwaggerSchemasResponse.php (1 hunks)
  • src/Subscription/Request/CreateAttributeDefinitionRequest.php (0 hunks)
  • src/Subscription/Request/CreateSubscriberListRequest.php (1 hunks)
  • src/Subscription/Request/CreateSubscriberRequest.php (1 hunks)
  • src/Subscription/Request/SubscribePageRequest.php (1 hunks)
  • src/Subscription/Request/SubscriberAttributeDefinitionRequest.php (1 hunks)
  • src/Subscription/Request/SubscribersExportRequest.php (2 hunks)
  • src/Subscription/Request/UpdateAttributeDefinitionRequest.php (0 hunks)
  • src/Subscription/Request/UpdateSubscriberRequest.php (1 hunks)
  • src/Subscription/Serializer/AttributeDefinitionNormalizer.php (1 hunks)
  • tests/Integration/Identity/Controller/AdminAttributeDefinitionControllerTest.php (2 hunks)
  • tests/Integration/Identity/Fixtures/AdminAttributeDefinitionFixture.php (0 hunks)
  • tests/Integration/Subscription/Controller/SubscriberAttributeDefinitionControllerTest.php (0 hunks)
  • tests/Integration/Subscription/Controller/SubscriberImportControllerTest.php (1 hunks)
  • tests/Integration/Subscription/Fixtures/SubscriberAttributeDefinitionFixture.php (2 hunks)
  • tests/Integration/Subscription/Fixtures/SubscriberAttributeValueFixture.php (2 hunks)
  • tests/Unit/Identity/Request/AdminAttributeDefinitionRequestTest.php (2 hunks)
  • tests/Unit/Identity/Request/UpdateAttributeDefinitionRequestTest.php (0 hunks)
  • tests/Unit/Subscription/Request/CreateAttributeDefinitionRequestTest.php (0 hunks)
  • tests/Unit/Subscription/Request/SubscriberAttributeDefinitionRequestTest.php (1 hunks)
  • tests/Unit/Subscription/Request/UpdateAttributeDefinitionRequestTest.php (0 hunks)
  • tests/Unit/Subscription/Serializer/AttributeDefinitionNormalizerTest.php (2 hunks)
💤 Files with no reviewable changes (9)
  • tests/Integration/Identity/Fixtures/AdminAttributeDefinitionFixture.php
  • src/Identity/Request/UpdateAttributeDefinitionRequest.php
  • tests/Integration/Subscription/Controller/SubscriberAttributeDefinitionControllerTest.php
  • src/Subscription/Request/UpdateAttributeDefinitionRequest.php
  • src/Subscription/OpenApi/SwaggerSchemasRequest.php
  • tests/Unit/Identity/Request/UpdateAttributeDefinitionRequestTest.php
  • src/Subscription/Request/CreateAttributeDefinitionRequest.php
  • tests/Unit/Subscription/Request/UpdateAttributeDefinitionRequestTest.php
  • tests/Unit/Subscription/Request/CreateAttributeDefinitionRequestTest.php
🧰 Additional context used
🧬 Code graph analysis (11)
tests/Unit/Subscription/Request/SubscriberAttributeDefinitionRequestTest.php (1)
src/Subscription/Request/SubscriberAttributeDefinitionRequest.php (1)
  • getDto (83-94)
src/Subscription/OpenApi/SwaggerSchemasResponse.php (1)
src/Subscription/Request/SubscriberAttributeDefinitionRequest.php (1)
  • OA (18-112)
src/Subscription/Request/SubscriberAttributeDefinitionRequest.php (3)
src/Subscription/OpenApi/SwaggerSchemasResponse.php (1)
  • OA (9-171)
src/Messaging/Request/CreateBounceRegexRequest.php (1)
  • Assert (44-62)
src/Identity/Request/AdminAttributeDefinitionRequest.php (1)
  • getDto (21-30)
src/Subscription/Request/UpdateSubscriberRequest.php (2)
src/Subscription/OpenApi/SwaggerSchemasResponse.php (1)
  • OA (9-171)
src/Subscription/Request/CreateSubscriberRequest.php (1)
  • OA (14-45)
src/Subscription/Request/CreateSubscriberListRequest.php (2)
src/Subscription/OpenApi/SwaggerSchemasResponse.php (1)
  • OA (9-171)
src/Subscription/Request/CreateSubscriberRequest.php (1)
  • OA (14-45)
tests/Unit/Identity/Request/AdminAttributeDefinitionRequestTest.php (1)
src/Identity/Request/AdminAttributeDefinitionRequest.php (1)
  • AdminAttributeDefinitionRequest (11-31)
src/Identity/Controller/AdminAttributeDefinitionController.php (2)
src/Identity/Request/AdminAttributeDefinitionRequest.php (1)
  • AdminAttributeDefinitionRequest (11-31)
src/Identity/OpenApi/SwaggerSchemasRequest.php (1)
  • OA (10-122)
src/Identity/OpenApi/SwaggerSchemasResponse.php (2)
src/Identity/OpenApi/SwaggerSchemasRequest.php (1)
  • OA (10-122)
src/Subscription/OpenApi/SwaggerSchemasResponse.php (1)
  • OA (9-171)
src/Subscription/Request/CreateSubscriberRequest.php (3)
src/Identity/OpenApi/SwaggerSchemasRequest.php (1)
  • OA (10-122)
src/Subscription/Request/CreateSubscriberListRequest.php (1)
  • OA (12-44)
src/Subscription/Request/UpdateSubscriberRequest.php (1)
  • OA (14-63)
src/Subscription/Controller/SubscriberAttributeDefinitionController.php (1)
src/Subscription/Request/SubscriberAttributeDefinitionRequest.php (1)
  • OA (18-112)
src/Identity/OpenApi/SwaggerSchemasRequest.php (2)
src/Identity/OpenApi/SwaggerSchemasResponse.php (1)
  • OA (9-48)
src/Subscription/Request/SubscriberAttributeDefinitionRequest.php (1)
  • OA (18-112)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: phpList Base Dist on PHP 8.1, with dist oldest [Build, Test]
  • GitHub Check: phpList Base Dist on PHP 8.1, with dist latest [Build, Test]
  • GitHub Check: Checkout phpList rest-api and generate docs specification (OpenAPI latest-restapi.json)
  • GitHub Check: generate-openapi
  • GitHub Check: phpList Base Dist on PHP 8.1, with dist latest [Build, Test]
🔇 Additional comments (23)
tests/Integration/Subscription/Controller/SubscriberImportControllerTest.php (2)

209-241: LGTM! Better user feedback on skipped emails.

The updated assertion correctly expects the error message for the skipped invalid email, which provides helpful feedback to users about what was skipped and why.


243-262: Verify that invalid emails should be importable when skip_invalid_emails is false.

The test expects that when skip_invalid_emails is false, the syntactically invalid email "invalid-email" is successfully imported with no errors. This could lead to data quality issues if invalid emails are stored in the system.

Please confirm whether this is the intended behavior or if email validation should still occur regardless of the skip_invalid_emails flag.

tests/Integration/Subscription/Fixtures/SubscriberAttributeDefinitionFixture.php (1)

10-19: LGTM! Consistent enum migration.

The changes mirror the previous fixture file and correctly use AttributeTypeEnum::Checkbox instead of the string literal. This maintains consistency across the test fixtures and leverages the type safety benefits of the enum.

tests/Integration/Subscription/Fixtures/SubscriberAttributeValueFixture.php (1)

10-21: Code changes are correct and enum migration is complete for this fixture.

The fixture properly uses AttributeTypeEnum::Checkbox instead of string literals. Verification across the codebase confirms no remaining string literal setType calls for SubscriberAttributeDefinition fixtures.

src/Subscription/Request/SubscribePageRequest.php (1)

16-17: Verified: No breaking changes detected in current codebase.

The change from ?bool $active to bool $active = false was investigated thoroughly:

  1. By default, Symfony Serializer adds null to nullable properties when parameters aren't provided, so the deserialization behavior does change—omitted fields now default to false instead of null.

  2. No null checks found: The codebase search found zero occurrences of null-checking on the $active property, indicating no code relies on distinguishing "not provided" from "explicitly false."

  3. Usage pattern safe: The property is passed directly to createPage() and updatePage() methods without conditional logic, suggesting the consuming code handles bool values correctly.

  4. Validation design confirms intent: The absence of NotBlank or NotNull constraints (only Type(bool)) suggests the field was intentionally made optional with a default value rather than required.

The change is intentional and appears safe for the current codebase, though the semantic shift (null → false for omitted fields) should be verified in integration/acceptance tests to confirm the business logic aligns with this default.

composer.json (1)

45-45: Verify the phplist/core branch reference.

The dependency shifted from dev-main to dev-dev. In Composer, this means switching from the main branch to a branch named dev. While unconventional (most projects use develop or development), confirm that this branch exists in the phpList/core repository and that the change aligns with your refactoring goals.

README.md (1)

41-41: LGTM! Documentation link updated to hosted docs.

The change to an externally hosted documentation URL is clear and improves accessibility for users.

src/Identity/OpenApi/SwaggerSchemasResponse.php (1)

30-30: LGTM! Example value now matches allowed enum types.

The type example is now consistent with the enum values defined in AdminAttributeDefinitionRequest (TextLine, Hidden).

src/Subscription/Request/CreateSubscriberRequest.php (1)

7-23: LGTM! OpenAPI schema annotation added.

The schema accurately documents the request structure without modifying runtime behavior.

src/Subscription/Request/CreateSubscriberListRequest.php (1)

7-22: LGTM! OpenAPI schema annotation added.

The schema accurately documents the request structure without modifying runtime behavior.

src/Subscription/Request/SubscribersExportRequest.php (2)

8-61: LGTM! Comprehensive OpenAPI schema annotation added.

The schema thoroughly documents the export request structure including all filtering options and column specifications.


120-129: LGTM! Improved DTO construction with named arguments.

Using named arguments instead of positional parameters improves code maintainability and reduces the risk of argument ordering errors.

tests/Unit/Subscription/Serializer/AttributeDefinitionNormalizerTest.php (2)

7-7: LGTM! Added enum import.

Necessary import for the AttributeTypeEnum usage in the test.


30-46: LGTM! Test updated for enum-based types and table_name removal.

The test correctly reflects:

  • Domain model now returns AttributeTypeEnum::Text
  • Normalizer converts enum to string 'text' for API responses
  • Removal of table_name field from serialized output
src/Identity/Request/AdminAttributeDefinitionRequest.php (1)

11-30: Refactoring verified and complete.

The consolidation of Create/Update requests into AdminAttributeDefinitionRequest has been successfully implemented throughout the codebase. The old class names (CreateAttributeDefinitionRequest and UpdateAttributeDefinitionRequest) are no longer referenced anywhere. The new unified class is properly integrated across:

  • Both create and update controller actions (lines 89-90 and 155-156 in AdminAttributeDefinitionController)
  • Test suite (AdminAttributeDefinitionRequestTest)
  • OpenAPI/Swagger documentation (SwaggerSchemasRequest)
  • DTO conversion logic (getDto() method returns AdminAttributeDefinitionDto correctly)

All references have been updated and there are no lingering dependencies on the old class names.

config/services/validators.yml (1)

40-42: Add missing validator tag and fix namespace mismatch.

The review comment is correct: AttributeTypeValidator at line 40 is missing the tags: ['validator.constraint_validator'] entry that all other validators in this file have (lines 7-38). The pattern is consistent—every constraint validator here includes this tag for Symfony's automatic registration.

Additionally, there's a namespace inconsistency: the config declares the service at PhpList\Core\Domain\Identity\Validator\AttributeTypeValidator, but the code imports and uses it from PhpList\Core\Domain\Subscription\Validator\AttributeTypeValidator (in src/Subscription/Request/SubscriberAttributeDefinitionRequest.php). Verify which namespace is correct and align them.

  PhpList\Core\Domain\Identity\Validator\AttributeTypeValidator:
    autowire: true
    autoconfigure: true
    tags: [ 'validator.constraint_validator' ]
src/Subscription/Serializer/AttributeDefinitionNormalizer.php (1)

21-28: Enum-backed type normalization is solid

Using $object->getType() ? $object->getType()->value : null cleanly bridges the enum-backed getType() to the string API field while staying null-safe. No issues from my side here.

tests/Integration/Identity/Controller/AdminAttributeDefinitionControllerTest.php (1)

30-47: Tests now correctly reflect enum-backed type values

The create/update tests using 'textline' and 'hidden' for type match the new Admin attribute definition contract and keep request/response expectations in sync. Looks good.

Also applies to: 49-65

src/Subscription/OpenApi/SwaggerSchemasResponse.php (1)

106-114: DynamicListAttrOption schema looks fine

The new DynamicListAttrOption schema (id, name, listorder) is straightforward and matches the intent for response-side option representations. No issues here from my end.

tests/Unit/Identity/Request/AdminAttributeDefinitionRequestTest.php (1)

7-45: Unit tests correctly exercise the new AdminAttributeDefinitionRequest

Both tests line up with AdminAttributeDefinitionRequest::getDto()—covering explicit values and defaulted/null fields—so they give good protection over the mapping logic after the refactor.

src/Identity/Controller/AdminAttributeDefinitionController.php (1)

15-15: Nice consolidation of request classes.

The refactoring to use a single AdminAttributeDefinitionRequest for both create and update operations reduces duplication and aligns with the pattern used in the Subscriber domain. The OpenAPI schema references and validator calls are consistently updated.

Also applies to: 53-53, 89-90, 109-109, 155-156

tests/Unit/Subscription/Request/SubscriberAttributeDefinitionRequestTest.php (1)

1-102: Solid test coverage for the new request class.

The test suite comprehensively covers DTO creation, default values, options handling, and validation scenarios. The tests verify both successful mappings (including enum conversion and options as DynamicListAttrDto) and validation failures for invalid inputs.

src/Subscription/Controller/SubscriberAttributeDefinitionController.php (1)

15-15: Clean consolidation matching the Admin domain pattern.

The refactoring to use a single SubscriberAttributeDefinitionRequest for both create and update operations is consistent with the approach in the Identity module. All OpenAPI schema references, type hints, and validator calls are properly updated.

Also applies to: 53-53, 87-88, 106-106, 152-153

@TatevikGr TatevikGr changed the title Dev Refactor attribute definition request handling Nov 17, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/Identity/Request/AdminAttributeDefinitionRequest.php (1)

47-47: Consider dependency injection for services (optional refactor).

Instantiating AttributeTypeValidator and IdentityTranslator directly makes testing harder and couples the validation logic to concrete implementations. Ideally, these would be injected dependencies. However, Symfony's #[Assert\Callback] doesn't support constructor injection easily, and the same pattern appears in SubscriberAttributeDefinitionRequest, so this may be an accepted pattern in the codebase.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3570df9 and 2537a92.

📒 Files selected for processing (5)
  • src/Identity/Request/AdminAttributeDefinitionRequest.php (1 hunks)
  • src/Subscription/OpenApi/SwaggerSchemasResponse.php (1 hunks)
  • src/Subscription/Request/SubscriberAttributeDefinitionRequest.php (1 hunks)
  • src/Subscription/Serializer/AttributeDefinitionNormalizer.php (2 hunks)
  • tests/Unit/Subscription/Serializer/AttributeDefinitionNormalizerTest.php (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/Subscription/OpenApi/SwaggerSchemasResponse.php
🧰 Additional context used
🧬 Code graph analysis (3)
tests/Unit/Subscription/Serializer/AttributeDefinitionNormalizerTest.php (1)
src/Subscription/Serializer/AttributeDefinitionNormalizer.php (2)
  • AttributeDefinitionNormalizer (11-54)
  • normalize (16-45)
src/Subscription/Request/SubscriberAttributeDefinitionRequest.php (2)
src/Subscription/OpenApi/SwaggerSchemasResponse.php (1)
  • OA (9-171)
src/Identity/Request/AdminAttributeDefinitionRequest.php (3)
  • Assert (15-57)
  • getDto (30-39)
  • validateType (41-56)
src/Identity/Request/AdminAttributeDefinitionRequest.php (1)
src/Subscription/Request/SubscriberAttributeDefinitionRequest.php (2)
  • getDto (83-94)
  • validateType (96-111)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: phpList Base Dist on PHP 8.1, with dist latest [Build, Test]
  • GitHub Check: phpList Base Dist on PHP 8.1, with dist oldest [Build, Test]
  • GitHub Check: phpList Base Dist on PHP 8.1, with dist oldest [Build, Test]
  • GitHub Check: phpList Base Dist on PHP 8.1, with dist latest [Build, Test]
🔇 Additional comments (4)
src/Identity/Request/AdminAttributeDefinitionRequest.php (2)

30-39: Couldn't locate DTO classes in the codebase—manual verification needed.

Searches for both AdminAttributeDefinitionDto and SubscriberAttributeDefinitionDto returned no results. I can't confirm the type parameter expectations without seeing the constructor signature.

Please verify:

  • Whether AdminAttributeDefinitionDto::__construct() expects type as a string or AttributeTypeEnum
  • If enum conversion is needed, align this with how SubscriberAttributeDefinitionRequest handles it

21-22: Verify type handling consistency and Choice constraint completeness.

The #[Assert\Choice] constraint at line 21 restricts input to only ['hidden', 'textline'], but SubscriberAttributeDefinitionRequest (a similar class) supports 9 attribute types via AttributeTypeEnum. Additionally, SubscriberAttributeDefinitionRequest converts the type string to an enum in getDto() (line 85: AttributeTypeEnum::from($this->type)), whereas AdminAttributeDefinitionRequest passes the raw string directly to AdminAttributeDefinitionDto. Confirm whether:

  1. The Choice constraint should include all enum values supported by AttributeTypeValidator
  2. The type should be converted to AttributeTypeEnum::from() before passing to the DTO (matching the pattern in SubscriberAttributeDefinitionRequest)
tests/Unit/Subscription/Serializer/AttributeDefinitionNormalizerTest.php (1)

7-9: Nice coverage for enum type and options normalization

The updated tests now cover both the simple case (no options) and the options path with DynamicListAttrDto instances, and they verify the enum type is normalized via AttributeTypeEnum as expected. Once the normalizer coalesces missing options to [], these tests give good confidence in the serializer behavior.

Also applies to: 26-49, 59-108

src/Subscription/Request/SubscriberAttributeDefinitionRequest.php (1)

18-49: No changes needed — swagger-php 4.11.0 handles enum cases directly

swagger-php v4.x accepts PHP 8.1 enum cases directly in OA\Property(enum: [...]), and for backed enums it automatically uses the case backing value (→value). The current code pattern is already correct and idiomatic; passing AttributeTypeEnum::Checkbox directly achieves the same result as AttributeTypeEnum::Checkbox->value with less verbosity.

Likely an incorrect or invalid review comment.

Comment on lines 22 to 44
$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(),
'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(),
'options' => $options,
];
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Default missing options to an empty array in the normalizer

As written, if SubscriberAttributeDefinition::getOptions() returns null, the normalized payload will contain 'options' => null, while the tests (and typical JSON APIs) expect [] when there are no options.

You can normalize this by coalescing to an empty array before mapping:

-        $options = $object->getOptions();
-        if (!empty($options)) {
+        $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);
         }

This keeps the wire format stable (options always an array) and matches the expectations in AttributeDefinitionNormalizerTest.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$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(),
'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(),
'options' => $options,
];
$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(),
'type' => $object->getType() ? $object->getType()->value : null,
'list_order' => $object->getListOrder(),
'default_value' => $object->getDefaultValue(),
'required' => $object->isRequired(),
'options' => $options,
];
🤖 Prompt for AI Agents
In src/Subscription/Serializer/AttributeDefinitionNormalizer.php around lines 22
to 44, getOptions() can return null which results in 'options' => null in the
normalized payload; coalesce the result to an empty array before mapping (e.g.
$options = $object->getOptions() ?? [];), then run the existing array_map logic
and always return 'options' as an array so empty option sets serialize as []
instead of null.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
src/Subscription/Serializer/AttributeDefinitionNormalizer.php (1)

22-31: Previous review comment still applies: normalize null options to an empty array.

If getOptions() returns null, the current code leaves $options as null, which then gets output as 'options' => null on line 40. Typical JSON APIs expect [] for empty collections. The previous reviewer suggested:

-        $options = $object->getOptions();
+        $options = $object->getOptions() ?? [];
         if (!empty($options)) {

Additionally, the array_map callback accesses properties directly ($option->id, $option->name, $option->listOrder) without type checking. If you want more defensive code, consider adding an instanceof DynamicListAttrDto check inside the callback.

🧹 Nitpick comments (1)
src/Subscription/Serializer/AttributeDefinitionNormalizer.php (1)

7-7: Unused import can be removed.

DynamicListAttrDto is imported but never referenced. If you're not planning to add type checks in the array_map callback, consider removing this import.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 454abed and bcff810.

📒 Files selected for processing (2)
  • src/Subscription/Serializer/AttributeDefinitionNormalizer.php (2 hunks)
  • tests/Unit/Subscription/Serializer/AttributeDefinitionNormalizerTest.php (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/Unit/Subscription/Serializer/AttributeDefinitionNormalizerTest.php
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: phpList Base Dist on PHP 8.1, with dist oldest [Build, Test]
  • GitHub Check: phpList Base Dist on PHP 8.1, with dist latest [Build, Test]
  • GitHub Check: phpList Base Dist on PHP 8.1, with dist latest [Build, Test]
  • GitHub Check: phpList Base Dist on PHP 8.1, with dist oldest [Build, Test]
🔇 Additional comments (1)
src/Subscription/Serializer/AttributeDefinitionNormalizer.php (1)

36-36: Type extraction looks good.

The null-safe approach correctly extracts the enum's scalar value or returns null when the type is not set.

@TatevikGr TatevikGr merged commit 3c41f7e into main Nov 17, 2025
8 of 9 checks passed
@TatevikGr TatevikGr deleted the dev branch November 17, 2025 10:33
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/Common/EventListener/ExceptionListener.php (1)

36-50: Consider consolidating duplicate exception handling logic.

There's a repeating pattern across multiple exception handlers where you create a JsonResponse with getMessage() and getStatusCode(). You could extract this into a helper method or create a common interface for these custom exceptions.

Something like:

private function createErrorResponse(\Throwable $exception, int $defaultStatusCode = 500): JsonResponse
{
    $statusCode = method_exists($exception, 'getStatusCode') 
        ? $exception->getStatusCode() 
        : $defaultStatusCode;
    
    return new JsonResponse([
        'message' => $exception->getMessage(),
    ], $statusCode);
}

Then you could simplify the handlers:

-        } elseif ($exception instanceof SubscriptionCreationException) {
-            $response = new JsonResponse([
-                'message' => $exception->getMessage(),
-            ], $exception->getStatusCode());
-            $event->setResponse($response);
-        } elseif ($exception instanceof AdminAttributeCreationException) {
-            $response = new JsonResponse([
-                '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 SubscriptionCreationException 
+                  || $exception instanceof AdminAttributeCreationException
+                  || $exception instanceof AttributeDefinitionCreationException) {
+            $event->setResponse($this->createErrorResponse($exception));

Not critical though—the current approach works fine.

src/Subscription/Controller/SubscriberAttributeDefinitionController.php (1)

352-357: Redundant database query—entity already hydrated via MapEntity

The MapEntity attribute on line 345 already loads and hydrates the SubscriberAttributeDefinition entity. Re-fetching it by name (lines 353-357) adds an unnecessary query and could introduce subtle bugs if the entity's name was modified but not yet persisted.

If the goal is to ensure relationships are loaded, use explicit joins in the repository or fetch-mode configuration instead:

-        /** @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

If lazy-loading is the issue, verify what relationships need hydration and address them at the repository level.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bcff810 and 3fcdb9c.

📒 Files selected for processing (4)
  • src/Common/EventListener/ExceptionListener.php (2 hunks)
  • src/Subscription/Controller/SubscriberAttributeDefinitionController.php (10 hunks)
  • src/Subscription/Request/SubscriberAttributeDefinitionRequest.php (1 hunks)
  • tests/Integration/Subscription/Controller/SubscriberAttributeDefinitionControllerTest.php (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/Subscription/Request/SubscriberAttributeDefinitionRequest.php (2)
src/Subscription/OpenApi/SwaggerSchemasResponse.php (1)
  • OA (9-171)
src/Identity/Request/AdminAttributeDefinitionRequest.php (3)
  • Assert (15-57)
  • getDto (30-39)
  • validateType (41-56)
tests/Integration/Subscription/Controller/SubscriberAttributeDefinitionControllerTest.php (2)
tests/Integration/Identity/Fixtures/AdministratorFixture.php (1)
  • AdministratorFixture (14-67)
tests/Integration/Identity/Fixtures/AdministratorTokenFixture.php (1)
  • AdministratorTokenFixture (15-62)
src/Subscription/Controller/SubscriberAttributeDefinitionController.php (3)
src/Common/Controller/BaseController.php (1)
  • BaseController (15-41)
src/Subscription/Serializer/AttributeDefinitionNormalizer.php (1)
  • AttributeDefinitionNormalizer (11-51)
src/Subscription/Request/SubscriberAttributeDefinitionRequest.php (1)
  • OA (18-115)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: phpList Base Dist on PHP 8.1, with dist oldest [Build, Test]
🔇 Additional comments (4)
src/Common/EventListener/ExceptionListener.php (2)

9-9: LGTM!

The import is correctly placed and necessary for the new exception handler.


46-50: LGTM!

The new exception handler follows the existing pattern correctly and will properly handle AttributeDefinitionCreationException by returning a JSON response with the appropriate status code.

tests/Integration/Subscription/Controller/SubscriberAttributeDefinitionControllerTest.php (1)

26-111: Test endpoint updates look good

The path updates from /api/v2/subscribers/attributes to /api/v2/attributes correctly align with the controller route refactoring. Test logic and assertions remain consistent.

src/Subscription/Controller/SubscriberAttributeDefinitionController.php (1)

77-81: Documented 409 response but conflict detection unclear — verify external dependency

The OpenAPI spec documents a 409 (conflict) response with AlreadyExistsResponse, but the create() method (lines 89–101) has no visible conflict detection. The actual conflict logic would exist in the injected AttributeDefinitionManager from the external phplist/core dependency (not in this codebase).

Verify that phplist/core's AttributeDefinitionManager::create() actually throws a ConflictHttpException when duplicate attribute definitions are detected. If it doesn't, either add validation here or remove the 409 response documentation.

Comment on lines +74 to +81
// Optional multi-value list for types like select/radio
#[Assert\Type('array')]
#[Assert\All([
'constraints' => [
new Assert\Type(['type' => DynamicListAttrDto::class]),
],
])]
public ?array $options = null;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Validation constraint mismatch: array elements won't be DynamicListAttrDto instances

The #[Assert\Type(['type' => DynamicListAttrDto::class])] constraint expects array elements to be instances of DynamicListAttrDto, but JSON request deserialization produces plain arrays, not class instances. This validation will fail when clients send valid JSON arrays for options.

Consider removing the class type constraint and validating the array structure instead:

-    #[Assert\Type('array')]
-    #[Assert\All([
-        'constraints' => [
-            new Assert\Type(['type' => DynamicListAttrDto::class]),
-        ],
-    ])]
+    #[Assert\Type('array')]
+    #[Assert\All([
+        new Assert\Collection(
+            fields: [
+                'id' => new Assert\Optional([new Assert\Type('integer')]),
+                'name' => [new Assert\NotBlank(), new Assert\Type('string')],
+                'list_order' => new Assert\Optional([new Assert\Type('integer')]),
+            ],
+            allowExtraFields: false,
+        ),
+    ])]
     public ?array $options = null;

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/Subscription/Request/SubscriberAttributeDefinitionRequest.php around
lines 74-81, the Assert\Type on array elements currently expects
DynamicListAttrDto instances but incoming JSON yields plain arrays, causing
valid requests to fail; remove the element-level Assert\Type that references
DynamicListAttrDto and replace it with validation that treats each element as an
array (e.g., use Assert\All with Assert\Type('array') and/or Assert\All with
Assert\Collection defining the expected keys and per-key constraints) so the
deserialized arrays are validated by structure rather than class instance.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants