Skip to content
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

Split ORMException into more fine-grained, better documented and typed exception classes #6743

Merged
merged 44 commits into from
Apr 13, 2018

Conversation

greg0ire
Copy link
Member

@greg0ire greg0ire commented Oct 2, 2017

This class is too big, and some exceptions could inherit from doctrine/common exceptions instead of this one.

TODO

  • remove unused exception methods
    • entityMissingAssignedIdForField
    • entityMissingForeignAssignedId
    • invalidFindByCall
    • invalidFlushMode
  • notSupported
  • namedNativeQueryNotFound
  • namedQueryNotFound
  • proxyClassesAlwaysRegenerating
  • invalidEntityRepository
  • unknownEntityNamespace
  • entityManagerClosed
  • invalidHydrationMode
  • mismatchedEventManager
  • missingIdentifierField
  • missingMappingDriverImpl
  • unrecognizedIdentifierFields
  • cantUseInOperatorOnCompositeKeys
  • invalidOrientation
  • unexpectedAssociationValue
  • unrecognizedField
  • findByRequiresParameter
  • invalidMagicCall
  • invalidFindByInverseAssociation
  • invalidResultCacheDriver
  • queryCacheNotConfigured
  • metadataCacheNotConfigured
  • queryCacheUsesNonPersistentCache
  • metadataCacheUsesNonPersistentCache

Proposal

method used in class should implement
notSupported Doctrine\ORM\Tools\SchemaTool SchemaToolException
namedNativeQueryNotFound Doctrine\ORM\Configuration ConfigurationException
namedQueryNotFound Doctrine\ORM\Configuration ConfigurationException
proxyClassesAlwaysRegenerating Doctrine\ORM\Configuration ConfigurationException
invalidEntityRepository Doctrine\ORM\Configuration ConfigurationException
unknownEntityNamespace Doctrine\ORM\Configuration ConfigurationException
entityManagerClosed Doctrine\ORM\EntityManager ManagerException
entityMissingAssignedIdForField no usages /dev/null
entityMissingForeignAssignedId no usages /dev/null
invalidHydrationMode Doctrine\ORM\EntityManager ManagerException
mismatchedEventManager Doctrine\ORM\EntityManager ManagerException
missingIdentifierField Doctrine\ORM\EntityManager ManagerException
missingMappingDriverImpl Doctrine\ORM\EntityManager ManagerException
unrecognizedIdentifierFields Doctrine\ORM\EntityManager ManagerException
cantUseInOperatorOnCompositeKeys Doctrine\ORM\Persisters\Entity\BasicEntityPersister PersisterException
invalidFlushMode no usages /dev/null
invalidOrientation Doctrine\ORM\Persisters\Entity\BasicEntityPersister PersisterException
unexpectedAssociationValue Doctrine\ORM\UnitOfWork ???
unrecognizedField Doctrine\ORM\Persisters\Entity\BasicEntityPersister PersisterException
findByRequiresParameter Doctrine\ORM\EntityRepository RepositoryException
invalidFindByCall no usages /dev/null
invalidMagicCall Doctrine\ORM\EntityRepository RepositoryException
invalidFindByInverseAssociation Doctrine\ORM\Persisters\Entity\BasicEntityPersister RepositoryException
invalidResultCacheDriver Doctrine\ORM\AbstractQuery CacheException
queryCacheNotConfigured Doctrine\ORM\Configuration CacheException
metadataCacheNotConfigured Doctrine\ORM\Configuration CacheException
queryCacheUsesNonPersistentCache Doctrine\ORM\Configuration CacheException
metadataCacheUsesNonPersistentCache Doctrine\ORM\Configuration CacheException

@greg0ire greg0ire force-pushed the split_orm_exception branch from 2169905 to 7898d3d Compare October 2, 2017 17:54
@Ocramius
Copy link
Member

Ocramius commented Oct 3, 2017

@greg0ire the split is indeed needed (see doctrine/common#681), but splitting just per component is a bit silly.

These are (almost) all separate exceptions, implementing a component-specific exception interface (for example, CacheException), and extend ORMException (need to do that for BC compliance).

For example:

class InvalidRepositoryMagicMethodCall extends ORMException implements RepositoryException
{
    public static function fromMethodCallAndArguments(string $method, array $arguments) : self {
        ...
    }
}

In an ideal world, this should be:

class InvalidRepositoryMagicMethodCall extends \InvalidArgumentException implements RepositoryException
{
    public static function fromMethodCallAndArguments(string $method, array $arguments) : self {
        ...
    }
}

Moving away from ORMException would be quite a BC break, so I don't think we can totally fix this.

@greg0ire
Copy link
Member Author

greg0ire commented Oct 3, 2017

Moving away from ORMException would be quite a BC break, so I don't think we can totally fix this.

@Ocramius My plan was to make ORMException an interface, and have all the split exceptions implement that interface. Is that a big BC-break? It's not for instanceof, or, more importantly, catch.

So to sum up, would that be ok with you:

class InvalidRepositoryMagicMethodCall extends \InvalidArgumentException implements RepositoryException, ORMException
{
    public static function fromMethodCallAndArguments(string $method, array $arguments) : self {
        ...
    }
}

@Ocramius
Copy link
Member

Ocramius commented Oct 3, 2017

@greg0ire given that the constructors would move around anyway, having an ORMException as an interface would be OK

@greg0ire
Copy link
Member Author

greg0ire commented Oct 3, 2017

Ok so if I understand correctly, I should move every method to its separate exception class, and use the split I proposed to create interfaces instead of classes. I'll try to push an example soon based on the "ideal world" snippet you gave + implements ORMException for BC

@Ocramius
Copy link
Member

Ocramius commented Oct 3, 2017

@greg0ire the implements ORMException can be done on the namespace-specific exception marker interface:

namespace Doctrine\ORM\Repository;

interface RepositoryException extends \Throwable, \Doctrine\ORM\ORMException
{
}

@greg0ire
Copy link
Member Author

greg0ire commented Oct 3, 2017

Even better!

@Majkl578
Copy link
Contributor

Majkl578 commented Oct 3, 2017

ORMException being an interface was exactly my idea.

namespace Doctrine\ORM;

interface ORMException extends \Throwable {}

Then specific exceptions would implement it (or have its own interface, as shown above):

namespace Doctrine\ORM\Mapping;

use  Doctrine\ORM\ORMException;

interface MappingException extends ORMException {}

class BlahException extends \RuntimeException implements MappingException {}

/**
* This interface should be implemented by all exceptions in this package.
*/
interface NewORMException extends \Throwable
Copy link
Member Author

Choose a reason for hiding this comment

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

Will of course be refactored to ORMException


use Doctrine\ORM\RepositoryException;

final class InvalidMagicMethodCall extends \InvalidArgumentException implements RepositoryException
Copy link
Member Author

Choose a reason for hiding this comment

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

I made the exception final

Copy link
Member Author

Choose a reason for hiding this comment

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

I removed the Repository bit from the name since it already is in the namespace.

Copy link
Member

Choose a reason for hiding this comment

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

Was InvalidArgumentException chosen instead of BadMethodCallException because of BC or because magic methods are getting the method name as an argument?

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't know, I copy pasted a snippet from @Ocramius but BadMethodCallException might be better indeed: it's not about the arguments being invalid, it's about the field not existing.


declare(strict_types=1);

namespace Doctrine\ORM\Repository;
Copy link
Member Author

Choose a reason for hiding this comment

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

I put the exception in the namespace of the relevant component.


declare(strict_types=1);

namespace Doctrine\ORM;
Copy link
Member Author

Choose a reason for hiding this comment

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

I put that at the top level namespace to avoid the repetition. An alternative would be to name the class Doctrine\ORM\Repository\Exception, tell me what you prefer.

Copy link
Member

Choose a reason for hiding this comment

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

We have a PersisterException in Doctrine\ORM\Persisters namespace. Also a CacheException in Doctrine\ORM\Cache. If we keep that structure, it suggests that RepositoryException will be in Doctrine\ORM\Repository.

I would like to see a structure as you suggested with Doctrine\ORM\Repository\Exception, even with the already existing exception classes, but I don't think that this would be an anticipated change.


final class InvalidMagicMethodCall extends \InvalidArgumentException implements RepositoryException
{
public static function fromEntityNameFieldNameAndMethod(
Copy link
Member Author

Choose a reason for hiding this comment

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

This method name is a bit long, I'm open to proposals

Copy link
Member

@SenseException SenseException Oct 4, 2017

Choose a reason for hiding this comment

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

fieldNameNotFound or fieldNotFound because it's the reason for not being able to use a magic method in an entity.

Copy link
Member Author

Choose a reason for hiding this comment

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

So no from?

Copy link
Member

Choose a reason for hiding this comment

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

The static name may lack fluent reading because of a missing method prefix like "from". Sounds funny with "because" InvalidMagicMethodCall::becauseFieldNotFoundIn, but I like "In" as suffix. Maybe the suffix can make it more readable without having needed arguments in the method name.

public static function fromEntityNameFieldNameAndMethod(
string $entityName,
string $fieldName,
$method
Copy link
Member Author

Choose a reason for hiding this comment

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

Woops I forgot the typehint here

@greg0ire greg0ire force-pushed the split_orm_exception branch from 7b6c3eb to 897ca26 Compare October 6, 2017 18:13

use Doctrine\ORM\RepositoryException;

final class InvalidFindByInverseAssociation extends \BadMethodCallException implements RepositoryException
Copy link
Member Author

Choose a reason for hiding this comment

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

I'm not a huge fan of this class name. I know I created it myself, but it's derived from the former method name. What I don't like is that it kind of implies there could be valid find by inverse association method calls. Should I drop the Invalid part?

Copy link
Member

Choose a reason for hiding this comment

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

I guess the name can be discussed in combination with the exception methods. I suggest InvalidAssociation or InvalidAssociationSide. I've read the exception message in the tests and used the mentioned names for it.

Copy link
Member Author

Choose a reason for hiding this comment

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

InvalidAssociationSide sounds great

Copy link
Member Author

Choose a reason for hiding this comment

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

But the other one leaves more room for other named constructors.


final class InvalidFindByInverseAssociation extends \BadMethodCallException implements RepositoryException
{
public static function becauseIsInverseAssociation(
Copy link
Member Author

@greg0ire greg0ire Oct 6, 2017

Choose a reason for hiding this comment

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

@Ocramius @SenseException can you please agree on a "because" vs "from" naming recommendation before I migrate all the exceptions?

Copy link
Member

@SenseException SenseException Oct 6, 2017

Choose a reason for hiding this comment

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

Like I previously wrote, "because" sounds too funny, so I can agree on a different prefix. If we go with my exception name suggestions of a previous comment about this class name, we can create a method like InvalidAssociationSide::fromInverseSide() or InvalidAssociation::fromInverseSideUsage(). We can recombine method name and class name between those two method-suggestions, if you like class name or method name, but not the other one.

Copy link
Member Author

@greg0ire greg0ire Oct 6, 2017

Choose a reason for hiding this comment

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

I don't think there is a point to using named constructors if we don't have several of them (or at least, leave room for more of them). I'll therefore go with your second proposition: classname = issue, and method name = cause of the issue.

Copy link
Member Author

Choose a reason for hiding this comment

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

Or rather, InvalidFindByCall::fromInverseSideUsage

Copy link
Member Author

Choose a reason for hiding this comment

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

I think I'm gonna cram findByRequiresParameter in there, Ocramius said those should be almost all separate exceptions, I think using the "almost" card here is right.

Copy link
Member Author

Choose a reason for hiding this comment

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

fromMissingParameter sounds weird though, maybe because is not that bad 🤔

Copy link
Member Author

@greg0ire greg0ire Oct 6, 2017

Choose a reason for hiding this comment

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

How about onMissingParameter, onInverseSideUsage?

Copy link
Member Author

Choose a reason for hiding this comment

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

I think I'm gonna cram findByRequiresParameter in there, Ocramius said those should be almost all separate exceptions, I think using the "almost" card here is right.

as @ostrolucky pointed out, this one is not about findBy, but about a magic call, so I guess I'm gonna put it in InvalidMagicCall instead

Copy link
Member

Choose a reason for hiding this comment

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

EvilMagicCall 😆
It sounds right that it belongs into an exception for magic calls.
About just one needed named constructor: They become or already are a standard in Doctrine, so it would be weird if we have one case where an instance was created with new. Especially if that exception class might need a second exception case one day, we have to look that the one with new will get an own named constructor too.

@@ -683,7 +684,7 @@ public function testCanRetrieveRepositoryFromClassNameWithLeadingBackslash()
/**
* @group DDC-1376
*
* @expectedException Doctrine\ORM\ORMException
* @expectedException Doctrine\ORM\Repository\InvalidFindByInverseAssociation
* @expectedExceptionMessage You cannot search for the association field 'Doctrine\Tests\Models\CMS\CmsUser#address', because it is the inverse side of an association.
Copy link
Member

Choose a reason for hiding this comment

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

I just realized it now, that one test is using $this->expectException() and $this->expectExceptionMessage() while we have annotations here.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think annotations should be completely dropped in favor of those method calls :P

@greg0ire
Copy link
Member Author

greg0ire commented Oct 6, 2017

Wait why is invalidFindByInverseAssociation only called in BasicEntityPersister? Does it really have to do with findBy?

@greg0ire
Copy link
Member Author

greg0ire commented Oct 7, 2017

Wait why is invalidFindByInverseAssociation only called in BasicEntityPersister? Does it really have to do with findBy?

Looking at the unit tests, it seems this exception is indeed supposed to be thrown when using findBy on a repo, so I guess it is ok.

@greg0ire greg0ire force-pushed the split_orm_exception branch 2 times, most recently from 678811a to 1f70547 Compare November 18, 2017 11:50
@greg0ire greg0ire force-pushed the split_orm_exception branch 2 times, most recently from 50d23b5 to 94e566c Compare November 30, 2017 18:25
@Majkl578
Copy link
Contributor

Majkl578 commented Dec 8, 2017

Needs a rebase.
Also I think all exceptions should be suffixed by Exception, at least for consistency.

@greg0ire greg0ire force-pushed the split_orm_exception branch from efc918a to 9040e93 Compare December 9, 2017 18:23
@greg0ire greg0ire force-pushed the split_orm_exception branch from 9044cda to 10aac31 Compare April 12, 2018 17:38
@Ocramius Ocramius self-assigned this Apr 13, 2018
Copy link
Member

@Ocramius Ocramius left a comment

Choose a reason for hiding this comment

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

This goes in as-is 👍

The BC breaks to be documented are to be added to the UPGRADE.md, but I'm raising a separate issue for that.

@Ocramius Ocramius dismissed Majkl578’s stale review April 13, 2018 15:17

Code review comments addressed.

@Ocramius Ocramius merged commit 77e3e5c into doctrine:master Apr 13, 2018
@Ocramius Ocramius changed the title Split orm exception Split ORMException into more fine-grained, better documented and typed exception types Apr 13, 2018
@Ocramius Ocramius changed the title Split ORMException into more fine-grained, better documented and typed exception types Split ORMException into more fine-grained, better documented and typed exception classes Apr 13, 2018
@Ocramius
Copy link
Member

Thanks @greg0ire! \o/ #7194

@greg0ire greg0ire deleted the split_orm_exception branch April 13, 2018 15:38
@greg0ire
Copy link
Member Author

Thanks a lot for merging! Will list the BC-break this week-end.

@Majkl578
Copy link
Contributor

Majkl578 commented May 1, 2018

@greg0ire Any update on documentating these changes?

@Majkl578
Copy link
Contributor

Majkl578 commented May 1, 2018

Also what do we do with all the exceptions left in root namespace?

lib/Doctrine/ORM/EntityNotFoundException.php
lib/Doctrine/ORM/NonUniqueResultException.php
lib/Doctrine/ORM/NoResultException.php
lib/Doctrine/ORM/OptimisticLockException.php
lib/Doctrine/ORM/ORMInvalidArgumentException.php
lib/Doctrine/ORM/PessimisticLockException.php
lib/Doctrine/ORM/TransactionRequiredException.php
lib/Doctrine/ORM/UnexpectedResultException.php

Current state is highly inconsistent.

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.

5 participants