Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

[WIP] DDC-1852 - Validating lifecycle callbacks in tools #361

Closed
wants to merge 8 commits into from

3 participants

@Ocramius
Owner

This feature simply adds validation for lifecycle callbacks.

This PR introduces some BC Breaks:

  1. Doctrine\ORM\Mapping\ClassMetadataInfo#validateLifecycleCallbacks() has been dropped
  2. Doctrine\ORM\Mapping\HasLifecycleCallbacks has been deprecated as it is not checked anymore. This makes the AnnotationDriver slower at first run, but I think this is where caching solves the problem correctly
  3. Mappings have to be validated via tools, as runtime validation is incomplete anyway. Runtime validation of lifecycle callbacks is dropped.

Build Status

@travisbot

This pull request passes (merged 0cd962d into 9445502).

@Ocramius
Owner

Weird that the branch alone fails...

@travisbot

This pull request passes (merged 403aae6 into 9445502).

@travisbot

This pull request passes (merged a9922e4 into 9445502).

@travisbot

This pull request passes (merged 33f832f into 9445502).

@travisbot

This pull request passes (merged 49c94551 into 9445502).

@travisbot

This pull request passes (merged a8e213bd into 7b75849).

@travisbot

This pull request passes (merged c7d0a99 into 7b75849).

@Ocramius
Owner

@beberlei can't think of any other addition to this. See if you like this idea :)

@beberlei beberlei commented on the diff
lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php
@@ -382,7 +382,6 @@ protected function validateRuntimeMetadata($class, $parent)
$class->validateIdentifier();
$class->validateAssocations();
- $class->validateLifecycleCallbacks($this->getReflectionService());
@beberlei Owner

Why remove this? I think it makes sense here.

@Ocramius Owner

Since validation has been moved to tools, this shouldn't be needed anymore here, plus it is a check that only worked on XML/YAML/PHP drivers.

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

As discussed on IRC, will (re-)add improved validation support in the ClassMetadataFactory :)

@beberlei
Owner

Close then?

@Ocramius
Owner

Sorry, missed the message. Closing for now :)

@Ocramius Ocramius closed this
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
View
10 UPGRADE_TO_2_3
@@ -24,4 +24,12 @@ Also, related functions were affected:
* iterate($parameters, $hydrationMode) the argument $parameters can be either an key=>value array or an ArrayCollection instance
* setParameters($parameters) the argument $parameters can be either an key=>value array or an ArrayCollection instance
* getParameters() now returns ArrayCollection instead of array
-* getParameter($key) now returns Parameter instance instead of parameter value
+* getParameter($key) now returns Parameter instance instead of parameter value
+
+# Lifecycle Callbacks
+
+The `@HasLifecycleCallbacks` has been deprecated: you can now put the lifecycle annotations on your entities' public
+methods without having to annotate the class itself. Also, the CLI tools can now detect invalid lifecycle callbacks.
+
+* Public method `Doctrine\ORM\Mapping\ClassMetadataInfo#validateLifecycleCallbacks` has been removed.
+* Public static method `Doctrine\ORM\Mapping\MappingException#lifecycleCallbackMethodNotFound` has been removed.
View
1  lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php
@@ -382,7 +382,6 @@ protected function validateRuntimeMetadata($class, $parent)
$class->validateIdentifier();
$class->validateAssocations();
- $class->validateLifecycleCallbacks($this->getReflectionService());
@beberlei Owner

Why remove this? I think it makes sense here.

@Ocramius Owner

Since validation has been moved to tools, this shouldn't be needed anymore here, plus it is a check that only worked on XML/YAML/PHP drivers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
// verify inheritance
if (!$class->isMappedSuperclass && !$class->isInheritanceTypeNone()) {
View
19 lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php
@@ -902,23 +902,6 @@ public function validateAssocations()
}
/**
- * Validate lifecycle callbacks
- *
- * @param ReflectionService $reflService
- * @return void
- */
- public function validateLifecycleCallbacks($reflService)
- {
- foreach ($this->lifecycleCallbacks as $event => $callbacks) {
- foreach ($callbacks as $callbackFuncName) {
- if ( ! $reflService->hasPublicMethod($this->name, $callbackFuncName)) {
- throw MappingException::lifecycleCallbackMethodNotFound($this->name, $callbackFuncName);
- }
- }
- }
- }
-
- /**
* Gets the ReflectionClass instance of the mapped class.
*
* @return ReflectionClass
@@ -1831,7 +1814,7 @@ public function setAssociationOverride($fieldName, array $overrideMapping)
$mapping['sourceToTargetKeyColumns'] = null;
$mapping['relationToSourceKeyColumns'] = null;
$mapping['relationToTargetKeyColumns'] = null;
-
+
switch ($mapping['type']) {
case self::ONE_TO_ONE:
$mapping = $this->_validateAndCompleteOneToOneMapping($mapping);
View
67 lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php
@@ -532,50 +532,47 @@ public function loadMetadataForClass($className, ClassMetadataInfo $metadata)
}
}
- // Evaluate @HasLifecycleCallbacks annotation
- if (isset($classAnnotations['Doctrine\ORM\Mapping\HasLifecycleCallbacks'])) {
- foreach ($class->getMethods() as $method) {
- // filter for the declaring class only, callbacks from parents will already be registered.
- if ($method->isPublic() && $method->getDeclaringClass()->getName() == $class->name) {
- $annotations = $this->_reader->getMethodAnnotations($method);
-
- if ($annotations && is_numeric(key($annotations))) {
- foreach ($annotations as $annot) {
- $annotations[get_class($annot)] = $annot;
- }
+ foreach ($class->getMethods() as $method) {
+ // filter for the declaring class only, callbacks from parents will already be registered.
+ if ($method->getDeclaringClass()->getName() == $class->name) {
+ $annotations = $this->_reader->getMethodAnnotations($method);
+
+ if ($annotations && is_numeric(key($annotations))) {
+ foreach ($annotations as $annot) {
+ $annotations[get_class($annot)] = $annot;
}
+ }
- if (isset($annotations['Doctrine\ORM\Mapping\PrePersist'])) {
- $metadata->addLifecycleCallback($method->getName(), \Doctrine\ORM\Events::prePersist);
- }
+ if (isset($annotations['Doctrine\ORM\Mapping\PrePersist'])) {
+ $metadata->addLifecycleCallback($method->getName(), \Doctrine\ORM\Events::prePersist);
+ }
- if (isset($annotations['Doctrine\ORM\Mapping\PostPersist'])) {
- $metadata->addLifecycleCallback($method->getName(), \Doctrine\ORM\Events::postPersist);
- }
+ if (isset($annotations['Doctrine\ORM\Mapping\PostPersist'])) {
+ $metadata->addLifecycleCallback($method->getName(), \Doctrine\ORM\Events::postPersist);
+ }
- if (isset($annotations['Doctrine\ORM\Mapping\PreUpdate'])) {
- $metadata->addLifecycleCallback($method->getName(), \Doctrine\ORM\Events::preUpdate);
- }
+ if (isset($annotations['Doctrine\ORM\Mapping\PreUpdate'])) {
+ $metadata->addLifecycleCallback($method->getName(), \Doctrine\ORM\Events::preUpdate);
+ }
- if (isset($annotations['Doctrine\ORM\Mapping\PostUpdate'])) {
- $metadata->addLifecycleCallback($method->getName(), \Doctrine\ORM\Events::postUpdate);
- }
+ if (isset($annotations['Doctrine\ORM\Mapping\PostUpdate'])) {
+ $metadata->addLifecycleCallback($method->getName(), \Doctrine\ORM\Events::postUpdate);
+ }
- if (isset($annotations['Doctrine\ORM\Mapping\PreRemove'])) {
- $metadata->addLifecycleCallback($method->getName(), \Doctrine\ORM\Events::preRemove);
- }
+ if (isset($annotations['Doctrine\ORM\Mapping\PreRemove'])) {
+ $metadata->addLifecycleCallback($method->getName(), \Doctrine\ORM\Events::preRemove);
+ }
- if (isset($annotations['Doctrine\ORM\Mapping\PostRemove'])) {
- $metadata->addLifecycleCallback($method->getName(), \Doctrine\ORM\Events::postRemove);
- }
+ if (isset($annotations['Doctrine\ORM\Mapping\PostRemove'])) {
+ $metadata->addLifecycleCallback($method->getName(), \Doctrine\ORM\Events::postRemove);
+ }
- if (isset($annotations['Doctrine\ORM\Mapping\PostLoad'])) {
- $metadata->addLifecycleCallback($method->getName(), \Doctrine\ORM\Events::postLoad);
- }
+ if (isset($annotations['Doctrine\ORM\Mapping\PostLoad'])) {
+ $metadata->addLifecycleCallback($method->getName(), \Doctrine\ORM\Events::postLoad);
+ }
- if (isset($annotations['Doctrine\ORM\Mapping\PreFlush'])) {
- $metadata->addLifecycleCallback($method->getName(), \Doctrine\ORM\Events::preFlush);
- }
+ if (isset($annotations['Doctrine\ORM\Mapping\PreFlush'])) {
+ $metadata->addLifecycleCallback($method->getName(), \Doctrine\ORM\Events::preFlush);
}
}
}
View
1  lib/Doctrine/ORM/Mapping/HasLifecycleCallbacks.php
@@ -20,6 +20,7 @@
namespace Doctrine\ORM\Mapping;
/**
+ * @deprecated This annotation is currently unused and will probably removed with future releases
* @Annotation
* @Target("CLASS")
*/
View
7 lib/Doctrine/ORM/Mapping/MappingException.php
@@ -144,7 +144,7 @@ public static function missingResultSetMappingFieldName($entity, $resultName)
{
return new self('Result set mapping named "'.$resultName.'" in "'.$entity.' requires a field name.');
}
-
+
public static function nameIsMandatoryForSqlResultSetMapping($className)
{
return new self("Result set mapping name on entity class '$className' is not defined.");
@@ -413,11 +413,6 @@ public static function mappedClassNotPartOfDiscriminatorMap($className, $rootCla
);
}
- public static function lifecycleCallbackMethodNotFound($className, $methodName)
- {
- return new self("Entity '" . $className . "' has no method '" . $methodName . "' to be registered as lifecycle callback.");
- }
-
public static function invalidFetchMode($className, $annotation)
{
return new self("Entity '" . $className . "' has a mapping with invalid fetch mode '" . $annotation . "'");
View
10 lib/Doctrine/ORM/Tools/EntityGenerator.php
@@ -37,7 +37,7 @@
* $generator->setUpdateEntityIfExists(true);
* $generator->generate($classes, '/path/to/generate/entities');
*
- *
+ *
* @link www.doctrine-project.org
* @since 2.0
* @author Benjamin Eberlei <kontakt@beberlei.de>
@@ -679,10 +679,6 @@ private function generateEntityDocBlock(ClassMetadataInfo $metadata)
if ($metadata->customRepositoryClassName) {
$lines[count($lines) - 1] .= '(repositoryClass="' . $metadata->customRepositoryClassName . '")';
}
-
- if (isset($metadata->lifecycleCallbacks) && $metadata->lifecycleCallbacks) {
- $lines[] = ' * @' . $this->annotationsPrefix . 'HasLifecycleCallbacks';
- }
}
$lines[] = ' */';
@@ -975,10 +971,10 @@ private function generateAssociationMappingPropertyDocBlock(array $associationMa
if ($this->generateAnnotations) {
$lines[] = $this->spaces . ' *';
-
+
if (isset($associationMapping['id']) && $associationMapping['id']) {
$lines[] = $this->spaces . ' * @' . $this->annotationsPrefix . 'Id';
-
+
if ($generatorType = $this->getIdGeneratorTypeString($metadata->generatorType)) {
$lines[] = $this->spaces . ' * @' . $this->annotationsPrefix . 'GeneratedValue(strategy="' . $generatorType . '")';
}
View
16 lib/Doctrine/ORM/Tools/SchemaValidator.php
@@ -249,6 +249,22 @@ public function validateClass(ClassMetadataInfo $class)
}
}
+ foreach ($class->lifecycleCallbacks as $name => $callbacks) {
+ foreach ($callbacks as $callback) {
+ if (!$class->reflClass->hasMethod($callback)) {
+ $ce[] = "A lifecycle callback for event '" . $name . "' is defined on non-existing method ".
+ " '" . $class->name . "#" . $callback . "'.";
+ continue;
+ }
+
+ $method = $class->reflClass->getMethod($callback);
+ if (!$method->isPublic()) {
+ $ce[] = "A lifecycle callback for event '" . $name . "' is defined on " . ($method->isProtected() ? 'protected' : 'private')
+ . " method '" . $class->name . "#" . $callback . "'. Only public methods can be used as callbacks.";
+ }
+ }
+ }
+
return $ce;
}
View
5 tests/Doctrine/Tests/ORM/Functional/LifecycleCallbackTest.php
@@ -184,7 +184,7 @@ public function testLifecycleListener_ChangeUpdateChangeSet()
}
}
-/** @Entity @HasLifecycleCallbacks */
+/** @Entity */
class LifecycleCallbackTestUser {
/** @Id @Column(type="integer") @GeneratedValue */
private $id;
@@ -203,7 +203,6 @@ public function testCallback() {$this->value = 'Hello World';}
/**
* @Entity
- * @HasLifecycleCallbacks
* @Table(name="lc_cb_test_entity")
*/
class LifecycleCallbackTestEntity
@@ -288,7 +287,7 @@ public function __construct()
}
}
-/** @MappedSuperclass @HasLifecycleCallbacks */
+/** @MappedSuperclass */
class LifecycleCallbackParentEntity {
/** @PrePersist */
function doStuff() {
View
6 tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1655Test.php
@@ -89,7 +89,6 @@ public function testPostLoadInheritanceChild()
* "foo" = "DDC1655Foo",
* "bar" = "DDC1655Bar"
* })
- * @HasLifecycleCallbacks
*/
class DDC1655Foo
{
@@ -112,10 +111,7 @@ public function postLoad()
}
}
-/**
- * @Entity
- * @HasLifecycleCallbacks
- */
+/** @Entity */
class DDC1655Bar extends DDC1655Foo
{
public $subLoaded;
View
1  tests/Doctrine/Tests/ORM/Functional/Ticket/DDC345Test.php
@@ -103,7 +103,6 @@ public function __construct()
/**
* @Entity
- * @HasLifecycleCallbacks
* @Table(name="ddc345_memberships", uniqueConstraints={
* @UniqueConstraint(name="ddc345_memship_fks", columns={"user_id","group_id"})
* })
View
1  tests/Doctrine/Tests/ORM/Functional/Ticket/DDC448Test.php
@@ -53,7 +53,6 @@ class DDC448MainTable
/**
* @Entity
* @Table(name="connectedClass")
- * @HasLifecycleCallbacks
*/
class DDC448ConnectedClass
{
View
15 tests/Doctrine/Tests/ORM/Mapping/AbstractMappingDriverTest.php
@@ -481,7 +481,7 @@ public function testIdentifierRequiredShouldMentionParentClasses()
{
$factory = $this->createClassMetadataFactory();
-
+
$factory->getMetadataFor('Doctrine\Tests\Models\DDC889\DDC889Entity');
}
@@ -490,7 +490,7 @@ public function testIdentifierRequiredShouldMentionParentClasses()
*/
public function testNamedNativeQuery()
{
-
+
$class = $this->createClassMetadata('Doctrine\Tests\Models\CMS\CmsAddress');
//named native query
@@ -519,7 +519,7 @@ public function testNamedNativeQuery()
$this->assertArrayHasKey('mapping-count', $class->sqlResultSetMappings);
$this->assertArrayHasKey('mapping-find-all', $class->sqlResultSetMappings);
$this->assertArrayHasKey('mapping-without-fields', $class->sqlResultSetMappings);
-
+
$findAllMapping = $class->getSqlResultSetMapping('mapping-find-all');
$this->assertEquals('mapping-find-all', $findAllMapping['name']);
$this->assertEquals('Doctrine\Tests\Models\CMS\CmsAddress', $findAllMapping['entities'][0]['entityClass']);
@@ -531,7 +531,7 @@ public function testNamedNativeQuery()
$this->assertEquals('mapping-without-fields', $withoutFieldsMapping['name']);
$this->assertEquals('Doctrine\Tests\Models\CMS\CmsAddress', $withoutFieldsMapping['entities'][0]['entityClass']);
$this->assertEquals(array(), $withoutFieldsMapping['entities'][0]['fields']);
-
+
$countMapping = $class->getSqlResultSetMapping('mapping-count');
$this->assertEquals('mapping-count', $countMapping['name']);
$this->assertEquals(array('name'=>'count'), $countMapping['columns'][0]);
@@ -619,7 +619,7 @@ public function testAssociationOverridesMapping()
$adminMetadata = $factory->getMetadataFor('Doctrine\Tests\Models\DDC964\DDC964Admin');
$guestMetadata = $factory->getMetadataFor('Doctrine\Tests\Models\DDC964\DDC964Guest');
-
+
// assert groups association mappings
$this->assertArrayHasKey('groups', $guestMetadata->associationMappings);
$this->assertArrayHasKey('groups', $adminMetadata->associationMappings);
@@ -678,7 +678,7 @@ public function testAssociationOverridesMapping()
$this->assertEquals($guestAddress['isCascadeRefresh'], $adminAddress['isCascadeRefresh']);
$this->assertEquals($guestAddress['isCascadeMerge'], $adminAddress['isCascadeMerge']);
$this->assertEquals($guestAddress['isCascadeDetach'], $adminAddress['isCascadeDetach']);
-
+
// assert override
$this->assertEquals('address_id', $guestAddress['joinColumns'][0]['name']);
$this->assertEquals(array('address_id'=>'id'), $guestAddress['sourceToTargetKeyColumns']);
@@ -735,7 +735,6 @@ public function testAttributeOverridesMapping()
/**
* @Entity
- * @HasLifecycleCallbacks
* @Table(
* name="cms_users",
* uniqueConstraints={@UniqueConstraint(name="search_idx", columns={"name", "user_email"})},
@@ -1037,7 +1036,7 @@ class DDC807Entity
* @GeneratedValue(strategy="NONE")
**/
public $id;
-
+
public static function loadMetadata(ClassMetadataInfo $metadata)
{
$metadata->mapField(array(
View
6 tests/Doctrine/Tests/ORM/Mapping/AnnotationDriverTest.php
@@ -262,7 +262,6 @@ class MappedSuperClassInheritence
* @Entity
* @InheritanceType("JOINED")
* @DiscriminatorMap({"parent" = "AnnotationParent", "child" = "AnnotationChild"})
- * @HasLifecycleCallbacks
*/
class AnnotationParent
{
@@ -288,10 +287,7 @@ public function preUpdate()
}
}
-/**
- * @Entity
- * @HasLifecycleCallbacks
- */
+/** @Entity */
class AnnotationChild extends AnnotationParent
{
View
19 tests/Doctrine/Tests/ORM/Mapping/ClassMetadataTest.php
@@ -340,7 +340,7 @@ public function testUnderscoreNamingStrategyDefaults()
'fieldName' => 'user',
'targetEntity' => 'CmsUser'
));
-
+
$this->assertEquals(array('USER_ID'=>'ID'), $oneToOneMetadata->associationMappings['user']['sourceToTargetKeyColumns']);
$this->assertEquals(array('USER_ID'=>'USER_ID'), $oneToOneMetadata->associationMappings['user']['joinColumnFieldNames']);
$this->assertEquals(array('ID'=>'USER_ID'), $oneToOneMetadata->associationMappings['user']['targetToSourceKeyColumns']);
@@ -348,7 +348,7 @@ public function testUnderscoreNamingStrategyDefaults()
$this->assertEquals('USER_ID', $oneToOneMetadata->associationMappings['user']['joinColumns'][0]['name']);
$this->assertEquals('ID', $oneToOneMetadata->associationMappings['user']['joinColumns'][0]['referencedColumnName']);
-
+
$this->assertEquals('CMS_ADDRESS_CMS_USER', $manyToManyMetadata->associationMappings['user']['joinTable']['name']);
$this->assertEquals(array('CMS_ADDRESS_ID','CMS_USER_ID'), $manyToManyMetadata->associationMappings['user']['joinTableColumns']);
@@ -770,7 +770,7 @@ public function testNamingCollisionSqlResultSetMappingShouldThrowException()
{
$cm = new ClassMetadata('Doctrine\Tests\Models\CMS\CmsUser');
$cm->initializeReflection(new \Doctrine\Common\Persistence\Mapping\RuntimeReflectionService);
-
+
$cm->addSqlResultSetMapping(array(
'name' => 'find-all',
'entities' => array(
@@ -803,19 +803,6 @@ public function testClassCaseSensitivity()
}
/**
- * @group DDC-659
- */
- public function testLifecycleCallbackNotFound()
- {
- $cm = new ClassMetadata('Doctrine\Tests\Models\CMS\CmsUser');
- $cm->initializeReflection(new \Doctrine\Common\Persistence\Mapping\RuntimeReflectionService);
- $cm->addLifecycleCallback('notfound', 'postLoad');
-
- $this->setExpectedException("Doctrine\ORM\Mapping\MappingException", "Entity 'Doctrine\Tests\Models\CMS\CmsUser' has no method 'notfound' to be registered as lifecycle callback.");
- $cm->validateLifecycleCallbacks(new \Doctrine\Common\Persistence\Mapping\RuntimeReflectionService);
- }
-
- /**
* @group ImproveErrorMessages
*/
public function testTargetEntityNotFound()
View
1  tests/Doctrine/Tests/ORM/Tools/Export/annotation/Doctrine.Tests.ORM.Tools.Export.User.php
@@ -4,7 +4,6 @@
/**
* @Entity
- * @HasLifecycleCallbacks
* @Table(name="cms_users")
*/
class User
View
44 tests/Doctrine/Tests/ORM/Tools/SchemaValidatorTest.php
@@ -9,7 +9,7 @@
class SchemaValidatorTest extends \Doctrine\Tests\OrmTestCase
{
/**
- * @var EntityManager
+ * @var \Doctrine\ORM\EntityManager
*/
private $em = null;
@@ -136,6 +136,31 @@ public function testInvalidTripleAssociationAsKeyMapping()
"The referenced column name 'id' has to be a primary key column on the target entity class 'Doctrine\Tests\ORM\Tools\DDC1649Two'."
), $ce);
}
+
+ /**
+ * @group DDC-1852
+ */
+ public function testInvalidLifecycleCallbacks()
+ {
+ $nonPublicCallbacks = $this->em->getClassMetadata(__NAMESPACE__ . '\DDC1852InvalidLifecycleCallbacks');
+
+ $nonPublicCallbacks->lifecycleCallbacks[\Doctrine\ORM\Events::prePersist][] = 'someNonExistingMethod';
+ $ce = $this->validator->validateClass($nonPublicCallbacks);
+
+ $this->assertEquals(
+ array(
+ "A lifecycle callback for event 'prePersist' is defined on private method '"
+ . __NAMESPACE__ . "\DDC1852InvalidLifecycleCallbacks#somePrivateMethod'."
+ . " Only public methods can be used as callbacks.",
+ "A lifecycle callback for event 'prePersist' is defined on protected method '"
+ . __NAMESPACE__ . "\DDC1852InvalidLifecycleCallbacks#someProtectedMethod'."
+ . " Only public methods can be used as callbacks.",
+ "A lifecycle callback for event 'prePersist' is defined on non-existing method '"
+ . __NAMESPACE__ . "\DDC1852InvalidLifecycleCallbacks#someNonExistingMethod'.",
+ ),
+ $ce
+ );
+ }
}
/**
@@ -204,7 +229,7 @@ class DDC1587ValidEntity1
private $name;
/**
- * @var Identifier
+ * @var DDC1587ValidEntity2
*
* @OneToOne(targetEntity="DDC1587ValidEntity2", cascade={"all"}, mappedBy="agent")
* @JoinColumn(name="pk", referencedColumnName="pk_agent")
@@ -265,3 +290,18 @@ class DDC1649Three
private $two;
}
+/**
+ * @Entity
+ */
+class DDC1852InvalidLifecycleCallbacks
+{
+ /** @Id @Column */
+ private $id;
+
+ /** @PrePersist */
+ private function somePrivateMethod() {}
+
+ /** @PrePersist */
+ protected function someProtectedMethod() {}
+}
+
Something went wrong with that request. Please try again.