Browse Source

Merge pull request #5972 from cakephp/3.0-aftersavecommit

3.0 afterSaveCommit
José Lorenzo Rodríguez 11 years ago
parent
commit
ca7ff37e6e

+ 10 - 0
src/Database/Connection.php

@@ -574,6 +574,16 @@ class Connection
     }
 
     /**
+     * Checks if a transaction is running.
+     *
+     * @return bool True if a transaction is runnning else false.
+     */
+    public function inTransaction()
+    {
+        return $this->_transactionStarted;
+    }
+
+    /**
      * Quotes value to be used safely in database query.
      *
      * @param mixed $value The value to quote.

+ 56 - 12
src/ORM/Table.php

@@ -1294,6 +1294,9 @@ class Table implements RepositoryInterface, EventListenerInterface
      *   listeners will receive the entity and the options array as arguments. The type
      *   of operation performed (insert or update) can be determined by checking the
      *   entity's method `isNew`, true meaning an insert and false an update.
+     * - Model.afterSaveCommit: Will be triggered after the transaction is commited
+     *   for atomic save, listeners will receive the entity and the options array
+     *   as arguments.
      *
      * This method will determine whether the passed entity needs to be
      * inserted or updated in the database. It does that by checking the `isNew`
@@ -1333,7 +1336,8 @@ class Table implements RepositoryInterface, EventListenerInterface
             'atomic' => true,
             'associated' => true,
             'checkRules' => true,
-            'checkExisting' => true
+            'checkExisting' => true,
+            '_primary' => true
         ]);
 
         if ($entity->errors()) {
@@ -1344,8 +1348,8 @@ class Table implements RepositoryInterface, EventListenerInterface
             return $entity;
         }
 
+        $connection = $this->connection();
         if ($options['atomic']) {
-            $connection = $this->connection();
             $success = $connection->transactional(function () use ($entity, $options) {
                 return $this->_processSave($entity, $options);
             });
@@ -1353,6 +1357,18 @@ class Table implements RepositoryInterface, EventListenerInterface
             $success = $this->_processSave($entity, $options);
         }
 
+        if ($success) {
+            if (!$connection->inTransaction() &&
+                ($options['atomic'] || (!$options['atomic'] && $options['_primary']))
+            ) {
+                $this->dispatchEvent('Model.afterSaveCommit', compact('entity', 'options'));
+            }
+            if ($options['atomic'] || $options['_primary']) {
+                $entity->isNew(false);
+                $entity->source($this->registryAlias());
+            }
+        }
+
         return $success;
     }
 
@@ -1393,7 +1409,7 @@ class Table implements RepositoryInterface, EventListenerInterface
             $this,
             $entity,
             $options['associated'],
-            $options->getArrayCopy()
+            ['_primary' => false] + $options->getArrayCopy()
         );
 
         if (!$saved && $options['atomic']) {
@@ -1414,13 +1430,15 @@ class Table implements RepositoryInterface, EventListenerInterface
                 $this,
                 $entity,
                 $options['associated'],
-                $options->getArrayCopy()
+                ['_primary' => false] + $options->getArrayCopy()
             );
             if ($success || !$options['atomic']) {
                 $entity->clean();
                 $this->dispatchEvent('Model.afterSave', compact('entity', 'options'));
-                $entity->isNew(false);
-                $entity->source($this->registryAlias());
+                if (!$options['atomic'] && !$options['_primary']) {
+                    $entity->isNew(false);
+                    $entity->source($this->registryAlias());
+                }
                 $success = true;
             }
         }
@@ -1570,10 +1588,14 @@ class Table implements RepositoryInterface, EventListenerInterface
      *
      * ### Events
      *
-     * - `beforeDelete` Fired before the delete occurs. If stopped the delete
+     * - `Model.beforeDelete` Fired before the delete occurs. If stopped the delete
      *   will be aborted. Receives the event, entity, and options.
-     * - `afterDelete` Fired after the delete has been successful. Receives
+     * - `Model.afterDelete` Fired after the delete has been successful. Receives
+     *   the event, entity, and options.
+     * - `Model.afterDelete` Fired after the delete has been successful. Receives
      *   the event, entity, and options.
+     * - `Model.afterDeleteCommit` Fired after the transaction is committed for
+     *   an atomic delete. Receives the event, entity, and options.
      *
      * The options argument will be converted into an \ArrayObject instance
      * for the duration of the callbacks, this allows listeners to modify
@@ -1582,16 +1604,33 @@ class Table implements RepositoryInterface, EventListenerInterface
      */
     public function delete(EntityInterface $entity, $options = [])
     {
-        $options = new ArrayObject($options + ['atomic' => true, 'checkRules' => true]);
+        $options = new ArrayObject($options + [
+            'atomic' => true,
+            'checkRules' => true,
+            '_primary' => true,
+        ]);
 
         $process = function () use ($entity, $options) {
             return $this->_processDelete($entity, $options);
         };
 
+        $connection = $this->connection();
         if ($options['atomic']) {
-            return $this->connection()->transactional($process);
+            $success = $connection->transactional($process);
+        } else {
+            $success = $process();
         }
-        return $process();
+
+        if ($success &&
+            !$connection->inTransaction() &&
+            ($options['atomic'] || (!$options['atomic'] && $options['_primary']))
+        ) {
+            $this->dispatchEvent('Model.afterDeleteCommit', [
+                'entity' => $entity,
+                'options' => $options
+            ]);
+        }
+        return $success;
     }
 
     /**
@@ -1631,7 +1670,10 @@ class Table implements RepositoryInterface, EventListenerInterface
             return $event->result;
         }
 
-        $this->_associations->cascadeDelete($entity, $options->getArrayCopy());
+        $this->_associations->cascadeDelete(
+            $entity,
+            ['_primary' => false] + $options->getArrayCopy()
+        );
 
         $query = $this->query();
         $conditions = (array)$entity->extract($primaryKey);
@@ -2147,8 +2189,10 @@ class Table implements RepositoryInterface, EventListenerInterface
             'Model.beforeFind' => 'beforeFind',
             'Model.beforeSave' => 'beforeSave',
             'Model.afterSave' => 'afterSave',
+            'Model.afterSaveCommit' => 'afterSaveCommit',
             'Model.beforeDelete' => 'beforeDelete',
             'Model.afterDelete' => 'afterDelete',
+            'Model.afterDeleteCommit' => 'afterDeleteCommit',
             'Model.beforeRules' => 'beforeRules',
             'Model.afterRules' => 'afterRules',
         ];

+ 58 - 0
tests/TestCase/Database/ConnectionTest.php

@@ -578,6 +578,64 @@ class ConnectionTest extends TestCase
     }
 
     /**
+     * Tests inTransaction()
+     *
+     * @return void
+     */
+    public function testInTransaction()
+    {
+        $this->connection->begin();
+        $this->assertTrue($this->connection->inTransaction());
+
+        $this->connection->begin();
+        $this->assertTrue($this->connection->inTransaction());
+
+        $this->connection->commit();
+        $this->assertTrue($this->connection->inTransaction());
+
+        $this->connection->commit();
+        $this->assertFalse($this->connection->inTransaction());
+
+        $this->connection->begin();
+        $this->assertTrue($this->connection->inTransaction());
+
+        $this->connection->begin();
+        $this->connection->rollback();
+        $this->assertFalse($this->connection->inTransaction());
+    }
+
+    /**
+     * Tests inTransaction() with save points
+     *
+     * @return void
+     */
+    public function testInTransactionWithSavePoints()
+    {
+        $this->skipIf(!$this->connection->useSavePoints(true));
+        $this->connection->begin();
+        $this->assertTrue($this->connection->inTransaction());
+
+        $this->connection->begin();
+        $this->assertTrue($this->connection->inTransaction());
+
+        $this->connection->commit();
+        $this->assertTrue($this->connection->inTransaction());
+
+        $this->connection->commit();
+        $this->assertFalse($this->connection->inTransaction());
+
+        $this->connection->begin();
+        $this->assertTrue($this->connection->inTransaction());
+
+        $this->connection->begin();
+        $this->connection->rollback();
+        $this->assertTrue($this->connection->inTransaction());
+
+        $this->connection->rollback();
+        $this->assertFalse($this->connection->inTransaction());
+    }
+
+    /**
      * Tests connection can quote values to be safely used in query strings
      *
      * @return void

+ 14 - 2
tests/TestCase/ORM/RulesCheckerIntegrationTest.php

@@ -461,7 +461,13 @@ class RulesCheckerIntegrationTest extends TestCase
         $table->eventManager()->attach(
             function ($event, Entity $entity, \ArrayObject $options, $operation) {
                 $this->assertEquals(
-                    ['atomic' => true, 'associated' => true, 'checkRules' => true, 'checkExisting' => true],
+                    [
+                        'atomic' => true,
+                        'associated' => true,
+                        'checkRules' => true,
+                        'checkExisting' => true,
+                        '_primary' => true,
+                    ],
                     $options->getArrayCopy()
                 );
                 $this->assertEquals('create', $operation);
@@ -494,7 +500,13 @@ class RulesCheckerIntegrationTest extends TestCase
         $table->eventManager()->attach(
             function ($event, Entity $entity, \ArrayObject $options, $result, $operation) {
                 $this->assertEquals(
-                    ['atomic' => true, 'associated' => true, 'checkRules' => true, 'checkExisting' => true],
+                    [
+                        'atomic' => true,
+                        'associated' => true,
+                        'checkRules' => true,
+                        'checkExisting' => true,
+                        '_primary' => true,
+                    ],
                     $options->getArrayCopy()
                 );
                 $this->assertEquals('create', $operation);

+ 266 - 48
tests/TestCase/ORM/TableTest.php

@@ -435,9 +435,12 @@ class TableTest extends TestCase
             'table' => 'users',
             'connection' => $this->connection,
         ]);
-        $table->eventManager()->attach(function ($event, $query, $options) {
-            $query->limit(1);
-        }, 'Model.beforeFind');
+        $table->eventManager()->on(
+            'Model.beforeFind',
+            function ($event, $query, $options) {
+                $query->limit(1);
+            }
+        );
 
         $result = $table->find('all')->all();
         $this->assertCount(1, $result, 'Should only have 1 record, limit 1 applied.');
@@ -456,10 +459,13 @@ class TableTest extends TestCase
             'connection' => $this->connection,
         ]);
         $expected = ['One', 'Two', 'Three'];
-        $table->eventManager()->attach(function ($event, $query, $options) use ($expected) {
-            $query->setResult($expected);
-            $event->stopPropagation();
-        }, 'Model.beforeFind');
+        $table->eventManager()->on(
+            'Model.beforeFind',
+            function ($event, $query, $options) use ($expected) {
+                $query->setResult($expected);
+                $event->stopPropagation();
+            }
+        );
 
         $query = $table->find('all');
         $query->limit(1);
@@ -1680,7 +1686,7 @@ class TableTest extends TestCase
             $this->assertSame($data, $entity);
             $entity->set('password', 'foo');
         };
-        $table->eventManager()->attach($listener, 'Model.beforeSave');
+        $table->eventManager()->on('Model.beforeSave', $listener);
         $this->assertSame($data, $table->save($data));
         $this->assertEquals($data->id, self::$nextUserId);
         $row = $table->find('all')->where(['id' => self::$nextUserId])->first();
@@ -1708,8 +1714,8 @@ class TableTest extends TestCase
         $listener2 = function ($e, $entity, $options) {
             $this->assertTrue($options['crazy']);
         };
-        $table->eventManager()->attach($listener1, 'Model.beforeSave');
-        $table->eventManager()->attach($listener2, 'Model.beforeSave');
+        $table->eventManager()->on('Model.beforeSave', $listener1);
+        $table->eventManager()->on('Model.beforeSave', $listener2);
         $this->assertSame($data, $table->save($data));
         $this->assertEquals($data->id, self::$nextUserId);
 
@@ -1736,7 +1742,7 @@ class TableTest extends TestCase
             $e->stopPropagation();
             return $entity;
         };
-        $table->eventManager()->attach($listener, 'Model.beforeSave');
+        $table->eventManager()->on('Model.beforeSave', $listener);
         $this->assertSame($data, $table->save($data));
         $this->assertNull($data->id);
         $row = $table->find('all')->where(['id' => self::$nextUserId])->first();
@@ -1763,10 +1769,104 @@ class TableTest extends TestCase
             $this->assertSame($data, $entity);
             $called = true;
         };
-        $table->eventManager()->attach($listener, 'Model.afterSave');
+        $table->eventManager()->on('Model.afterSave', $listener);
+
+        $calledAfterCommit = false;
+        $listenerAfterCommit = function ($e, $entity, $options) use ($data, &$calledAfterCommit) {
+            $this->assertSame($data, $entity);
+            $calledAfterCommit = true;
+        };
+        $table->eventManager()->on('Model.afterSaveCommit', $listenerAfterCommit);
+
         $this->assertSame($data, $table->save($data));
         $this->assertEquals($data->id, self::$nextUserId);
         $this->assertTrue($called);
+        $this->assertTrue($calledAfterCommit);
+    }
+
+    /**
+     * Asserts that afterSaveCommit is also triggered for non-atomic saves
+     *
+     * @return void
+     */
+    public function testAfterSaveCommitForNonAtomic()
+    {
+        $table = TableRegistry::get('users');
+        $data = new \Cake\ORM\Entity([
+            'username' => 'superuser',
+            'created' => new Time('2013-10-10 00:00'),
+            'updated' => new Time('2013-10-10 00:00')
+        ]);
+
+        $called = false;
+        $listener = function ($e, $entity, $options) use ($data, &$called) {
+            $this->assertSame($data, $entity);
+            $called = true;
+        };
+        $table->eventManager()->on('Model.afterSave', $listener);
+
+        $calledAfterCommit = false;
+        $listenerAfterCommit = function ($e, $entity, $options) use ($data, &$calledAfterCommit) {
+            $calledAfterCommit = true;
+        };
+        $table->eventManager()->on('Model.afterSaveCommit', $listenerAfterCommit);
+
+        $this->assertSame($data, $table->save($data, ['atomic' => false]));
+        $this->assertEquals($data->id, self::$nextUserId);
+        $this->assertTrue($called);
+        $this->assertTrue($calledAfterCommit);
+    }
+
+    /**
+     * Asserts the afterSaveCommit is not triggered if transaction is running.
+     *
+     * @return void
+     */
+    public function testAfterSaveCommitWithTransactionRunning()
+    {
+        $table = TableRegistry::get('users');
+        $data = new \Cake\ORM\Entity([
+            'username' => 'superuser',
+            'created' => new Time('2013-10-10 00:00'),
+            'updated' => new Time('2013-10-10 00:00')
+        ]);
+
+        $called = false;
+        $listener = function ($e, $entity, $options) use (&$called) {
+            $called = true;
+        };
+        $table->eventManager()->on('Model.afterSaveCommit', $listener);
+
+        $this->connection->begin();
+        $this->assertSame($data, $table->save($data));
+        $this->assertFalse($called);
+        $this->connection->commit();
+    }
+
+    /**
+     * Asserts the afterSaveCommit is not triggered if transaction is running.
+     *
+     * @return void
+     */
+    public function testAfterSaveCommitWithNonAtomicAndTransactionRunning()
+    {
+        $table = TableRegistry::get('users');
+        $data = new \Cake\ORM\Entity([
+            'username' => 'superuser',
+            'created' => new Time('2013-10-10 00:00'),
+            'updated' => new Time('2013-10-10 00:00')
+        ]);
+
+        $called = false;
+        $listener = function ($e, $entity, $options) use (&$called) {
+            $called = true;
+        };
+        $table->eventManager()->on('Model.afterSaveCommit', $listener);
+
+        $this->connection->begin();
+        $this->assertSame($data, $table->save($data, ['atomic' => false]));
+        $this->assertFalse($called);
+        $this->connection->commit();
     }
 
     /**
@@ -1807,9 +1907,55 @@ class TableTest extends TestCase
         $listener = function ($e, $entity, $options) use ($data, &$called) {
             $called = true;
         };
-        $table->eventManager()->attach($listener, 'Model.afterSave');
+        $table->eventManager()->on('Model.afterSave', $listener);
+
+        $calledAfterCommit = false;
+        $listenerAfterCommit = function ($e, $entity, $options) use ($data, &$calledAfterCommit) {
+            $calledAfterCommit = true;
+        };
+        $table->eventManager()->on('Model.afterSaveCommit', $listenerAfterCommit);
+
         $this->assertFalse($table->save($data));
         $this->assertFalse($called);
+        $this->assertFalse($calledAfterCommit);
+    }
+
+    /**
+     * Asserts that afterSaveCommit callback is triggered only for primary table
+     *
+     * @group save
+     * @return void
+     */
+    public function testAfterSaveCommitTriggeredOnlyForPrimaryTable()
+    {
+        $entity = new \Cake\ORM\Entity([
+            'title' => 'A Title',
+            'body' => 'A body'
+        ]);
+        $entity->author = new \Cake\ORM\Entity([
+            'name' => 'Jose'
+        ]);
+
+        $table = TableRegistry::get('articles');
+        $table->belongsTo('authors');
+
+        $calledForArticle = false;
+        $listenerForArticle = function ($e, $entity, $options) use (&$calledForArticle) {
+            $calledForArticle = true;
+        };
+        $table->eventManager()->on('Model.afterSaveCommit', $listenerForArticle);
+
+        $calledForAuthor = false;
+        $listenerForAuthor = function ($e, $entity, $options) use (&$calledForAuthor) {
+            $calledForAuthor = true;
+        };
+        $table->authors->eventManager()->on('Model.afterSaveCommit', $listenerForAuthor);
+
+        $this->assertSame($entity, $table->save($entity));
+        $this->assertFalse($entity->isNew());
+        $this->assertFalse($entity->author->isNew());
+        $this->assertTrue($calledForArticle);
+        $this->assertFalse($calledForAuthor);
     }
 
     /**
@@ -2070,7 +2216,7 @@ class TableTest extends TestCase
             $this->assertFalse($entity->isNew());
             $called = true;
         };
-        $table->eventManager()->attach($listener, 'Model.beforeSave');
+        $table->eventManager()->on('Model.beforeSave', $listener);
         $this->assertSame($entity, $table->save($entity));
         $this->assertTrue($called);
     }
@@ -2321,12 +2467,12 @@ class TableTest extends TestCase
     public function testDeleteCallbacks()
     {
         $entity = new \Cake\ORM\Entity(['id' => 1, 'name' => 'mark']);
-        $options = new \ArrayObject(['atomic' => true, 'checkRules' => false]);
+        $options = new \ArrayObject(['atomic' => true, 'checkRules' => false, '_primary' => true]);
 
         $mock = $this->getMock('Cake\Event\EventManager');
 
         $mock->expects($this->at(0))
-            ->method('attach');
+            ->method('on');
 
         $mock->expects($this->at(1))
             ->method('dispatch');
@@ -2351,12 +2497,84 @@ class TableTest extends TestCase
                 )
             ));
 
+        $mock->expects($this->at(4))
+            ->method('dispatch')
+            ->with($this->logicalAnd(
+                $this->attributeEqualTo('_name', 'Model.afterDeleteCommit'),
+                $this->attributeEqualTo(
+                    'data',
+                    ['entity' => $entity, 'options' => $options]
+                )
+            ));
+
         $table = TableRegistry::get('users', ['eventManager' => $mock]);
         $entity->isNew(false);
         $table->delete($entity, ['checkRules' => false]);
     }
 
     /**
+     * Test afterDeleteCommit is also called for non-atomic delete
+     *
+     * @return void
+     */
+    public function testDeleteCallbacksNonAtomic()
+    {
+        $table = TableRegistry::get('users');
+
+        $data = $table->get(1);
+        $options = new \ArrayObject(['atomic' => false, 'checkRules' => false]);
+
+        $called = false;
+        $listener = function ($e, $entity, $options) use ($data, &$called) {
+            $this->assertSame($data, $entity);
+            $called = true;
+        };
+        $table->eventManager()->on('Model.afterDelete', $listener);
+
+        $calledAfterCommit = false;
+        $listenerAfterCommit = function ($e, $entity, $options) use ($data, &$calledAfterCommit) {
+            $calledAfterCommit = true;
+        };
+        $table->eventManager()->on('Model.afterDeleteCommit', $listenerAfterCommit);
+
+        $table->delete($data, ['atomic' => false]);
+        $this->assertTrue($called);
+        $this->assertTrue($calledAfterCommit);
+    }
+
+    /**
+     * Test that afterDeleteCommit is only triggered for primary table
+     *
+     * @return void
+     */
+    public function testAfterDeleteCommitTriggeredOnlyForPrimaryTable()
+    {
+        $table = TableRegistry::get('authors');
+        $table->hasOne('articles', [
+            'foreignKey' => 'author_id',
+            'dependent' => true,
+        ]);
+
+        $called = false;
+        $listener = function ($e, $entity, $options) use (&$called) {
+            $called = true;
+        };
+        $table->eventManager()->on('Model.afterDeleteCommit', $listener);
+
+        $called2 = false;
+        $listener = function ($e, $entity, $options) use (&$called2) {
+            $called2 = true;
+        };
+        $table->articles->eventManager()->on('Model.afterDeleteCommit', $listener);
+
+        $entity = $table->get(1);
+        $this->assertTrue($table->delete($entity));
+
+        $this->assertTrue($called);
+        $this->assertFalse($called2);
+    }
+
+    /**
      * Test delete beforeDelete can abort the delete.
      *
      * @return void
@@ -3588,7 +3806,7 @@ class TableTest extends TestCase
         $cb = function ($event) use (&$count) {
             $count++;
         };
-        EventManager::instance()->attach($cb, 'Model.initialize');
+        EventManager::instance()->on('Model.initialize', $cb);
         $articles = TableRegistry::get('Articles');
 
         $this->assertEquals(1, $count, 'Callback should be called');
@@ -3621,7 +3839,7 @@ class TableTest extends TestCase
         $cb = function ($event) use (&$count) {
             $count++;
         };
-        EventManager::instance()->attach($cb, 'Model.buildValidator');
+        EventManager::instance()->on('Model.buildValidator', $cb);
         $articles = TableRegistry::get('Articles');
         $articles->validator();
         $this->assertEquals(1, $count, 'Callback should be called');
@@ -3700,33 +3918,33 @@ class TableTest extends TestCase
         $eventManager = $table->eventManager();
 
         $associationBeforeFindCount = 0;
-        $table->association('authors')->target()->eventManager()->attach(
+        $table->association('authors')->target()->eventManager()->on(
+            'Model.beforeFind',
             function (Event $event, Query $query, ArrayObject $options, $primary) use (&$associationBeforeFindCount) {
                 $this->assertTrue(is_bool($primary));
                 $associationBeforeFindCount ++;
-            },
-            'Model.beforeFind'
+            }
         );
 
         $beforeFindCount = 0;
-        $eventManager->attach(
+        $eventManager->on(
+            'Model.beforeFind',
             function (Event $event, Query $query, ArrayObject $options, $primary) use (&$beforeFindCount) {
                 $this->assertTrue(is_bool($primary));
                 $beforeFindCount ++;
-            },
-            'Model.beforeFind'
+            }
         );
         $table->find()->contain('authors')->first();
         $this->assertEquals(1, $associationBeforeFindCount);
         $this->assertEquals(1, $beforeFindCount);
 
         $buildValidatorCount = 0;
-        $eventManager->attach(
+        $eventManager->on(
+            'Model.buildValidator',
             $callback = function (Event $event, Validator $validator, $name) use (&$buildValidatorCount) {
                 $this->assertTrue(is_string($name));
                 $buildValidatorCount ++;
-            },
-            'Model.buildValidator'
+            }
         );
         $table->validator();
         $this->assertEquals(1, $buildValidatorCount);
@@ -3736,38 +3954,38 @@ class TableTest extends TestCase
         $afterRulesCount =
         $beforeSaveCount =
         $afterSaveCount = 0;
-        $eventManager->attach(
+        $eventManager->on(
+            'Model.buildRules',
             function (Event $event, RulesChecker $rules) use (&$buildRulesCount) {
                 $buildRulesCount ++;
-            },
-            'Model.buildRules'
+            }
         );
-        $eventManager->attach(
+        $eventManager->on(
+            'Model.beforeRules',
             function (Event $event, Entity $entity, ArrayObject $options, $operation) use (&$beforeRulesCount) {
                 $this->assertTrue(is_string($operation));
                 $beforeRulesCount ++;
-            },
-            'Model.beforeRules'
+            }
         );
-        $eventManager->attach(
+        $eventManager->on(
+            'Model.afterRules',
             function (Event $event, Entity $entity, ArrayObject $options, $result, $operation) use (&$afterRulesCount) {
                 $this->assertTrue(is_bool($result));
                 $this->assertTrue(is_string($operation));
                 $afterRulesCount ++;
-            },
-            'Model.afterRules'
+            }
         );
-        $eventManager->attach(
+        $eventManager->on(
+            'Model.beforeSave',
             function (Event $event, Entity $entity, ArrayObject $options) use (&$beforeSaveCount) {
                 $beforeSaveCount ++;
-            },
-            'Model.beforeSave'
+            }
         );
-        $eventManager->attach(
+        $eventManager->on(
+            'Model.afterSave',
             $afterSaveCallback = function (Event $event, Entity $entity, ArrayObject $options) use (&$afterSaveCount) {
                 $afterSaveCount ++;
-            },
-            'Model.afterSave'
+            }
         );
         $entity = new Entity(['title' => 'Title']);
         $this->assertNotFalse($table->save($entity));
@@ -3779,17 +3997,17 @@ class TableTest extends TestCase
 
         $beforeDeleteCount =
         $afterDeleteCount = 0;
-        $eventManager->attach(
+        $eventManager->on(
+            'Model.beforeDelete',
             function (Event $event, Entity $entity, ArrayObject $options) use (&$beforeDeleteCount) {
                 $beforeDeleteCount ++;
-            },
-            'Model.beforeDelete'
+            }
         );
-        $eventManager->attach(
+        $eventManager->on(
+            'Model.afterDelete',
             function (Event $event, Entity $entity, ArrayObject $options) use (&$afterDeleteCount) {
                 $afterDeleteCount ++;
-            },
-            'Model.afterDelete'
+            }
         );
         $this->assertTrue($table->delete($entity, ['checkRules' => false]));
         $this->assertEquals(1, $beforeDeleteCount);