Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[Uid] Use more concrete exception classes #60154

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed

Conversation

rela589n
Copy link
Contributor

@rela589n rela589n commented Apr 5, 2025

Q A
Branch? 7.3
Bug fix? no
New feature? yes
Deprecations? no
License MIT

Hello

This PR introduces two exception classes: InvalidUuidException and InvalidUlidException that replace \InvalidArgumentException thrown from the constructor.

In particular, this is useful if we rely on exceptions as a primary source of validation.

@carsonbot
Copy link

Hey!

I see that this is your first PR. That is great! Welcome!

Symfony has a contribution guide which I suggest you to read.

In short:

  • Always add tests
  • Keep backward compatibility (see https://symfony.com/bc).
  • Bug fixes must be submitted against the lowest maintained branch where they apply (see https://symfony.com/releases)
  • Features and deprecations must be submitted against the 7.3 branch.

Review the GitHub status checks of your pull request and try to solve the reported issues. If some tests are failing, try to see if they are failing because of this change.

When two Symfony core team members approve this change, it will be merged and you will become an official Symfony contributor!
If this PR is merged in a lower version branch, it will be merged up to all maintained branches within a few days.

I am going to sit back now and wait for the reviews.

Cheers!

Carsonbot

rela589n and others added 2 commits April 7, 2025 09:10
@rela589n rela589n force-pushed the feat-more-concrete-uid-exception-classes branch from 6752938 to 3802394 Compare April 7, 2025 06:10
Copy link
Member

@nicolas-grekas nicolas-grekas left a comment

Choose a reason for hiding this comment

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

What about a single InvalidUidException?

Copy link
Member

@nicolas-grekas nicolas-grekas left a comment

Choose a reason for hiding this comment

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

Thinking again about this: can you expand on the use case of exposing invalid type+value info? Since they're invalid, I'm not sure it make sense (you still have the original value when calling the methods that throw if really needed.)

@rela589n
Copy link
Contributor Author

rela589n commented Apr 8, 2025

Since they're invalid, I'm not sure it make sense (you still have the original value when calling the methods that throw if really needed.)

Actually you're right that client code isn't likely to benefit from having these methods, since usually you don't catch these exceptions by yourself.

Yet, it makes a lot of sense when you are using exception handling library.
Right now I'm developing exceptional-validation library that automates mapping of the exceptions to the respective properties.

Herewith, Value - is the nitty gritty thing we can use to distinguish between two properties both of which are to contain uuid.

For example, if we do not distinguish by value, then we won't be able to properly map InvalidUuidException to the second property:

#[ExceptionalValidation]
class TransferMoneyCommand
{
      #[Capture(InvalidUuidException::class)]
      #[Capture(NotEnoughMoneyException::class)]
      public readonly string $fromCardId;

      #[Capture(InvalidUuidException::class)]
      public readonly string $toCardId;
}

// note that second uuid is invalid
$command = new TransferMoneyCommand('eb27966e-b78f-7afc-96c5-cd6e18c37616', '!not valid!');

try {
    // handler will call `Uuid::fromString()` for both `fromCardId` and `toCardId`
    $commandBus->process($command);

    // middleware throws exception with violations created from mapped exceptions
} catch (ExceptionalValidationFailedException $e) {
    $violationList = $e->getViolationList();

    // This violation property path is incorrect, since invalid value was in `toCardId`, not `fromCardId`...
    // $violationList[0].propertyPath = 'fromCardId'
    // $violationList[0].invalidValue = '!not valid!'
}

Yet, if we had getValue() method, we would be able to use InvalidUuidValueMatchCondition that'd have compared $e->getValue() with the actual value of the property in question:

final class InvalidUuidValueMatchCondition implements MatchCondition
{
    public function __construct(
        private readonly mixed $value,
    ) {
    }

    /** @param InvalidUuidException $exception */
    public function matches(Throwable $exception): bool
    {
        return $exception->getValue() === $this->value;
    }
}

And then, if $fromCardId specifies this condition, '!not valid!' value won't match to this property, but will match to $toCardId instead:

#[Capture(InvalidUuidException::class, condition: InvalidUuidValueMatchCondition::class)]
#[Capture(NotEnoughMoneyException::class)]
public readonly string $fromCardId;

Thereby, it results in correct property path mapping:

try {
// ...
} catch (ExceptionalValidationFailedException $e) {
    $violationList = $e->getViolationList();

    // Correct:
    // $violationList[0].propertyPath = 'toCardId'
    // $violationList[0].invalidValue = '!not valid!'
}

@nicolas-grekas
Copy link
Member

Shouldn't you match by name instead of value?
Let's assume we don't add the getters, what alternative will you implement?

@rela589n
Copy link
Contributor Author

rela589n commented Apr 8, 2025

Shouldn't you match by name instead of value?

What name do you mean to match by? Name of the exception? Yes, we match by exception name two properties: fromCardId and toCardId, but it's not enough, since if InvalidUuidException was thrown when trying to create Uuid from the value of toCardId, it would be ascribed to fromCardId, while should've been ascribed to toCardId.

Let's assume we don't add the getters, what alternative will you implement?

If we do not add getters, it would be necessary to parse the exception message in order to get the value, and that's why I'm proposing this PR so that it'd be possible to match by exception value w/o parsing anything.

Maybe it's not so clear what I'm talking about, if it's the case, I think I could come up with some diagram that will explain what I'm trying to do.

@nicolas-grekas
Copy link
Member

I thought about property names.

Don't miss this comment also:

What about a single InvalidUidException?

@rela589n
Copy link
Contributor Author

rela589n commented Apr 9, 2025

I thought about property names.

Sorry if it's not clear right off the way. I'll try to put it a little more easier way.

Let's see two approaches that should lead to the same result.

First - standard validation. It looks like this:

class TransferMoneyCommand
{
    #[Assert\Uuid()]
    public string $fromCardId;

    #[Assert\Uuid()]
    public string $toCardId;
}

Having object (fromCardId = 'dba3ed3f-0a78-79fa-91d6-1697049dce0d', toCardId = 'invalid') results in violation at toCardId.

Second approach is exceptional validation, looks like this:

class TransferMoneyCommand
{
    #[Capture(InvalidUuidException::class)]
    public string $fromCardId;

    #[Capture(InvalidUuidException::class)]
    public string $toCardId;
}

The same object (fromCardId = 'dba3ed3f-0a78-79fa-91d6-1697049dce0d', toCardId = 'invalid') should in violation at toCardId.

Here violation is based on already thrown exception from the client code, and more explanation can be found here on the diagram.

TLDR, it's namely the following:

// code to get property path:

try {
    // @@ $stack->next($envelope)
} catch (InvalidUuidException $exception) {
    $propertyPath = null;

    if ($exception->getValue() === $command->fromCardId) {
        $propertyPath = 'fromCardId';
    }
    if ($exception->getValue() === $command->toCardId) {
        $propertyPath = 'toCardId';
    }
    // @@ process the violation
}

Therefore it's not possible to know where this InvalidUuidException originated from unless we have value in it.

If there's getValue(), we can match with fromCardId and toCardId to know where it originated from.

Please, let me know this clarifies why getValue() is essential.

@nicolas-grekas
Copy link
Member

This looks fragile design to me. Somewhere is the stack, it would make sense to be able to know which property you're checking, without guessing by value. Could be by wrapping the Uid exception in another one that gives the property path for example. That might be a better way forward to cover your need.
Would that make any sense to you?

@rela589n
Copy link
Contributor Author

rela589n commented Apr 9, 2025

Could be by wrapping the Uid exception in another one that gives the property path for example

do you mean creating one exception per each property type we check?

@nicolas-grekas
Copy link
Member

I'm not int the inner of the validation lib, but at some point, properties are going to be checked one by one, and at each step, one knows the corresponding propertyPath. Can't this info be used in some violation object?

@rela589n
Copy link
Contributor Author

rela589n commented Apr 9, 2025

I'm not int the inner of the validation lib, but at some point, properties are going to be checked one by one, and at each step, one knows the corresponding propertyPath. Can't this info be used in some violation object?

The point here is that it's not the validation lib. It's exception processing lib.
Flow is following:

  1. client code throws the exception (we do not control it)
  2. lib catches it
  3. it iterates over object properties one by one to know where this exception belongs
  4. if we have found the corresponding mapping, it's finally it - we create constraint violation object for that property

@rela589n
Copy link
Contributor Author

rela589n commented Apr 9, 2025

What about a single InvalidUidException?

Regarding InvalidUidException, IMO it's better to keep them separate.

For example, assuming we use single exception, there could be edge case when there are both ulid and uuid in single place:

class SomeCommand
{
    #[Capture(InvalidUidException::class)]
    public string $ulid;

    #[Capture(InvalidUidException::class)]
    public string $uuid;
}

If client code gets SomeCommand(ulid=01H5Z7XQ6Y3K4J2W8V9N0M1P2A, uuid=01H5Z7XQ6Y3K4J2W8V9N0M1P2A), it will result in InvalidUidException when creating Uuid::fromString():

$ulid = Ulid::fromString($command->ulid);
$uuid = Uuid::fromString($command->uuid); // throws

This exception should be mapped to uuid, as it was from this property that it originated.

Yet, since #[Capture attributes are the same (same classname), and properties use the same value, it will be mapped incorrectly (ascribed to ulid, while should've been to uuid).

Therefore, if possible, I'd ask to keep them as separate exceptions.

@smnandre
Copy link
Member

smnandre commented Apr 9, 2025

I thought about property names.

Don't miss this comment also:

What about a single InvalidUidException?

👍

@rela589n
Copy link
Contributor Author

rela589n commented Apr 9, 2025

Hi, @smnandre,

I think you have missed my latest response regarding InvalidUidException

Could you please check it?

@stof
Copy link
Member

stof commented Apr 9, 2025

@rela589n I don't see why having 2 separate extension classes is necessary to handle the case of having 1 Uuid property and 1 Ulid property. Wouldn't you have the same issue in case you have 2 Uuid properties (which will still throw the same exception class) ?

@nicolas-grekas
Copy link
Member

nicolas-grekas commented Apr 9, 2025

The more I ask (thanks a lot for your answer), the more I find the motivation for the PR inadequate for Symfony. E.g the argument to split in two base types is for your lib to be able to differentiate those two cases more accurately. This has nothing to do with why Symfony should do it.

I'm sorry to say so, but I feel like the way this lib works is by doing fragile guesswork. The ideal world for this lib would be one where every exception reports every details about it in a structured form. Since this world doesn't exist, I understand why you're contributing some improvements to Symfony: it's widely used so making it report back what the lib needs has a nice impact on your side.

Yet, this vision looks fundamentally unachievable to me, because it relies on ideally being able to patch all exceptions thrown by every existing libs that could end up being turned into validation errors.

About the PR, to still help move forward: I'm fine throwing component-specific base types for the component. We do so already in other components. But not for reporting back detailed stuff just because one lib with a questionable approach needs that, sorry.

@rela589n
Copy link
Contributor Author

rela589n commented Apr 9, 2025

why having 2 separate extension classes is necessary to handle the case of having 1 Uuid property and 1 Ulid property

This gives context of the value (uuid or ulid), so that InvalidUuidException can be caught for invalid Uuid (while not Ulid), and vice versa.

Wouldn't you have the same issue in case you have 2 Uuid properties (which will still throw the same exception class) ?

It wouldn't. Having two uuid string properties with the same value, it wouldn't matter if exception is mapped to the first property, or to the second, as both are invalid.

In case of two different property values, it wouldn't either, since value is considered as well.

@rela589n
Copy link
Contributor Author

rela589n commented Apr 9, 2025

find the motivation for the PR inadequate for Symfony.
This has nothing to do with why Symfony should do it.

I acknowledge that Symfony is your framework, and it's completely up to you to decide what will come in, and what will not.

I'm sorry to say so, but I feel like the way this lib works is by doing fragile guesswork.

You don't have to be soory, I understand that your opinion differs from mine, and I'm sure that you have prerequisites for it.

The ideal world for this lib would be one where every exception reports every details about it in a structured form.
it relies on ideally being able to patch all exceptions thrown by every existing libs that could end up being turned into validation errors

Actually, this isn't that big.
Righ now I see only two things that could end up turned into validation errors.

First is Uuid, which is the mostly used entity identifier.
Second is ValidationFailedException, which already has everything needed.

That's more than 90% of most commonly needed things. The rest 10% can be solved with custom exception in the client code.

why you're contributing some improvements to Symfony: it's widely used so making it report back what the lib needs has a nice impact on your side

Yes, that's why I contribute. Symfony is the best framework, suitable for the best tasks on the best projects.
That lib, which I develop isn't for 90% of the projects currently developed. It's for those 10% that use DDD.
If it had a good integration with Symfony, it would help a lot of people to lower their cost of development in DDD, allowing the usage SF components in the Domain layer w/o the need to implement a lot of stuff. SF Validator is good at validation, and I think it's the one best to use in the Domain.

My point is following: this change would allow many people to benefit from a beautiful domain, expressed with SF Components, backed by this lib on the level above.

I recognize that this approach might not be reasonalbe for you, and you might not like it, and it is completely in your sovereignty to support or reject it. Yet, if you would relent on this, it would mean really a lot to me, and I would be really thankful.

Copy link
Member

@nicolas-grekas nicolas-grekas left a comment

Choose a reason for hiding this comment

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

Let me close as I think parsing the message will work well enough for you and the change doesn't make much sense on the side of Symfony to me.
Thanks for you understanding and for submitting!

@rela589n
Copy link
Contributor Author

@nicolas-grekas , I still desire to have it as a part of symfony

Is there particular reason for not having it?

nicolas-grekas added a commit that referenced this pull request Apr 17, 2025
This PR was merged into the 7.3 branch.

Discussion
----------

[Uid] Add component-specific exception classes

| Q             | A
| ------------- | ---
| Branch?       | 7.3
| Bug fix?      | no
| New feature?  | yes
| Deprecations? | no
| Issues        | -
| License       | MIT

Generalization of #60154

/cc `@rela589n`

Commits
-------

e86ffe3 [Uid] Add component-specific exception classes
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants