diff --git a/Cake/Cache/CacheRegistry.php b/Cake/Cache/CacheRegistry.php index a7542a0912f..2d9bdeeb117 100644 --- a/Cake/Cache/CacheRegistry.php +++ b/Cake/Cache/CacheRegistry.php @@ -59,12 +59,13 @@ protected function _throwMissingClassError($class, $plugin) { * * Part of the template method for Cake\Utility\ObjectRegistry::load() * @param string|CacheEngine $class The classname or object to make. + * @param string $alias The alias of the object. * @param array $settings An array of settings to use for the cache engine. * @return CacheEngine The constructed CacheEngine class. * @throws Cake\Error\Exception when an object doesn't implement * the correct interface. */ - protected function _create($class, $settings) { + protected function _create($class, $alias, $settings) { if (is_object($class)) { $instance = $class; } diff --git a/Cake/Console/TaskRegistry.php b/Cake/Console/TaskRegistry.php index 3351923780f..539de3576e2 100644 --- a/Cake/Console/TaskRegistry.php +++ b/Cake/Console/TaskRegistry.php @@ -73,11 +73,12 @@ protected function _throwMissingClassError($class, $plugin) { * * Part of the template method for Cake\Utility\ObjectRegistry::load() * - * @param string $class The classname that is missing. + * @param string $class The classname to create. + * @param string $alias The alias of the task. * @param array $settings An array of settings to use for the task. * @return Component The constructed task class. */ - protected function _create($class, $settings) { + protected function _create($class, $alias, $settings) { return new $class( $this->_Shell->stdout, $this->_Shell->stderr, diff --git a/Cake/Controller/ComponentRegistry.php b/Cake/Controller/ComponentRegistry.php index f4716db0f93..47b56ef2634 100644 --- a/Cake/Controller/ComponentRegistry.php +++ b/Cake/Controller/ComponentRegistry.php @@ -98,11 +98,12 @@ protected function _throwMissingClassError($class, $plugin) { * Part of the template method for Cake\Utility\ObjectRegistry::load() * Enabled components will be registered with the event manager. * - * @param string $class The classname that is missing. + * @param string $class The classname to create. + * @param string $alias The alias of the component. * @param array $settings An array of settings to use for the component. * @return Component The constructed component class. */ - protected function _create($class, $settings) { + protected function _create($class, $alias, $settings) { $instance = new $class($this, $settings); $enable = isset($settings['enabled']) ? $settings['enabled'] : true; if ($enable) { diff --git a/Cake/Database/ConnectionRegistry.php b/Cake/Database/ConnectionRegistry.php index a6c61fc1ca1..58964cd78f8 100644 --- a/Cake/Database/ConnectionRegistry.php +++ b/Cake/Database/ConnectionRegistry.php @@ -66,10 +66,11 @@ protected function _throwMissingClassError($class, $plugin) { * Part of the template method for Cake\Utility\ObjectRegistry::load() * * @param string|Driver $class The classname or object to make. + * @param string $alias The alias of the object. * @param array $settings An array of settings to use for the driver. * @return Connection A connection with the correct driver. */ - protected function _create($class, $settings) { + protected function _create($class, $alias, $settings) { if (is_object($class)) { $instance = $class; } diff --git a/Cake/Log/LogEngineRegistry.php b/Cake/Log/LogEngineRegistry.php index 875854bc19b..2b66830633f 100644 --- a/Cake/Log/LogEngineRegistry.php +++ b/Cake/Log/LogEngineRegistry.php @@ -58,12 +58,13 @@ protected function _throwMissingClassError($class, $plugin) { * * Part of the template method for Cake\Utility\ObjectRegistry::load() * @param string|LogInterface $class The classname or object to make. + * @param string $alias The alias of the object. * @param array $settings An array of settings to use for the logger. * @return LogEngine The constructed logger class. * @throws Cake\Error\Exception when an object doesn't implement * the correct interface. */ - protected function _create($class, $settings) { + protected function _create($class, $alias, $settings) { if (is_object($class)) { $instance = $class; } diff --git a/Cake/ORM/Behavior.php b/Cake/ORM/Behavior.php new file mode 100644 index 00000000000..217e73188aa --- /dev/null +++ b/Cake/ORM/Behavior.php @@ -0,0 +1,162 @@ +doSomething($arg1, $arg2);`. + * + * ## Callback methods + * + * Behaviors can listen to any events fired on a Table. By default + * CakePHP provides a number of lifecycle events your behaviors can + * listen to: + * + * - `beforeFind(Event $event, Query $query)` + * Fired before a query is converted into SQL. + * + * - `beforeDelete(Event $event, Entity $entity)` + * Fired before an entity is deleted. + * + * - `afterDelete(Event $event, Entity $entity)` + * Fired after an entity has been deleted. The entity parameter + * will contain the entity state from before it was deleted. + * + * - `beforeSave(Event $event, Entity $entity)` + * Fired before an entity is saved. In the case where + * multiple entities are being saved, one event will be fired + * for each entity. + * + * - `afterSave(Event $event, Entity $entity)` + * Fired after an entity is saved. The saved entity will be provided + * as a parameter. + * + * In addition to the core events, behaviors can respond to any + * event fired from your Table classes including custom application + * specific ones. + * + * You can set the priority of a behaviors callbacks by using the + * `priority` setting when attaching a behavior. This will set the + * priority for all the callbacks a behavior provides. + * + * ## Finder methods + * + * Behaviors can provide finder methods that hook into a Table's + * find() method. Custom finders are a great way to provide preset + * queries that relate to your behavior. For example a SluggableBehavior + * could provide a find('slugged') finder. Behavior finders + * are implemented the same as other finders. Any method + * starting with `find` will be setup as a finder. Your finder + * methods should expect the following arguments: + * + * {{{ + * findSlugged(Query $query, array $options = []) + * }}} + * + * + * @see Cake\ORM\Table::addBehavior() + * @see Cake\Event\EventManager + */ +class Behavior implements EventListener { + +/** + * Contains configuration settings. + * + * @var array + */ + protected $_settings = []; + +/** + * Constructor + * + * Does not retain a reference to the Table object. If you need this + * you should override the constructor. + * + * @param Table $table The table this behavior is attached to. + * @param array $settings The settings for this behavior. + */ + public function __construct(Table $table, array $settings = []) { + $this->_settings = $settings; + } + +/** + * Read the settings being used. + * + * @return array + */ + public function settings() { + return $this->_settings; + } + +/** + * Get the Model callbacks this behavior is interested in. + * + * By defining one of the callback methods a behavior is assumed + * to be interested in the related event. + * + * Override this method if you need to add non-conventional event listeners. + * Or if you want you behavior to listen to non-standard events. + * + * @return array + */ + public function implementedEvents() { + $eventMap = [ + 'Model.beforeFind' => 'beforeFind', + 'Model.beforeSave' => 'beforeSave', + 'Model.afterSave' => 'afterSave', + 'Model.beforeDelete' => 'beforeDelete', + 'Model.afterDelete' => 'afterDelete', + ]; + $settings = $this->settings(); + $priority = isset($settings['priority']) ? $settings['priority'] : null; + $events = []; + + foreach ($eventMap as $event => $method) { + if (!method_exists($this, $method)) { + continue; + } + if ($priority === null) { + $events[$event] = $method; + } else { + $events[$event] = [ + 'callable' => $method, + 'priority' => $priority + ]; + } + } + return $events; + } + +} diff --git a/Cake/ORM/BehaviorRegistry.php b/Cake/ORM/BehaviorRegistry.php new file mode 100644 index 00000000000..55c34d7ee07 --- /dev/null +++ b/Cake/ORM/BehaviorRegistry.php @@ -0,0 +1,247 @@ +_table = $table; + $this->_eventManager = $table->getEventManager(); + } + +/** + * Resolve a behavior classname. + * + * Part of the template method for Cake\Utility\ObjectRegistry::load() + * + * @param string $class Partial classname to resolve. + * @return string|false Either the correct classname or false. + */ + protected function _resolveClassName($class) { + return App::classname($class, 'Model/Behavior', 'Behavior'); + } + +/** + * Throws an exception when a behavior is missing. + * + * Part of the template method for Cake\Utility\ObjectRegistry::load() + * + * @param string $class The classname that is missing. + * @param string $plugin The plugin the behavior is missing in. + * @throws Cake\Error\MissingBehaviorException + */ + protected function _throwMissingClassError($class, $plugin) { + throw new Error\MissingBehaviorException([ + 'class' => $class, + 'plugin' => $plugin + ]); + } + +/** + * Create the behavior instance. + * + * Part of the template method for Cake\Utility\ObjectRegistry::load() + * Enabled behaviors will be registered with the event manager. + * + * @param string $class The classname that is missing. + * @param string $alias The alias of the object. + * @param array $settings An array of settings to use for the behavior. + * @return Behavior The constructed behavior class. + */ + protected function _create($class, $alias, $settings) { + $instance = new $class($this->_table, $settings); + $enable = isset($settings['enabled']) ? $settings['enabled'] : true; + if ($enable) { + $this->_eventManager->attach($instance); + } + $methods = $this->_getMethods($instance, $class, $alias); + $this->_methodMap = array_merge($this->_methodMap, $methods['methods']); + $this->_finderMap = array_merge($this->_finderMap, $methods['finders']); + return $instance; + } + +/** + * Get the behavior methods and ensure there are no duplicates. + * + * Use the implementedEvents() method to exclude callback methods. + * Methods starting with `_` will be ignored, as will methods + * declared on Cake\ORM\Behavior + * + * @param Cake\ORM\Behavior $instance + * @return void + * @throws Cake\Error\Exception when duplicate methods are connected. + */ + protected function _getMethods(Behavior $instance, $class, $alias) { + if (isset(static::$_methodCache[$class])) { + return static::$_methodCache[$class]; + } + $events = $instance->implementedEvents(); + $reflection = new \ReflectionClass($class); + + $eventMethods = $methodMap = $finderMap = []; + foreach ($events as $e => $binding) { + if (is_array($binding) && isset($binding['callable']) && isset($binding['callable'])) { + $binding = $binding['callable']; + } + $eventMethods[$binding] = true; + } + + foreach ($reflection->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) { + if ($method->getDeclaringClass()->getName() === 'Cake\ORM\Behavior') { + continue; + } + + $methodName = $method->getName(); + if (strpos($methodName, '_') === 0 || isset($eventMethods[$methodName])) { + continue; + } + $methodName = strtolower($methodName); + + if (isset($this->_finderMap[$methodName]) || isset($this->_methodMap[$methodName])) { + if (isset($this->_finderMap[$methodName])) { + $duplicate = $this->_finderMap[$methodName]; + } else { + $duplicate = $this->_methodMap[$methodName]; + } + $error = __d( + 'cake_dev', + '%s contains duplicate method "%s" which is already provided by "%s"', + $class, + $method->getName(), + $duplicate + ); + throw new Error\Exception($error); + } + + $isFinder = substr($methodName, 0, 4) === 'find'; + if ($isFinder) { + $finderMap[$methodName] = $alias; + } else { + $methodMap[$methodName] = $alias; + } + } + static::$_methodCache[$class] = ['methods' => $methodMap, 'finders' => $finderMap]; + return static::$_methodCache[$class]; + } + +/** + * Check if any loaded behavior implements a method. + * + * Will return true if any behavior provides a public non-finder method + * with the chosen name. + * + * @param string $method The method to check for. + * @return boolean + */ + public function hasMethod($method) { + $method = strtolower($method); + return isset($this->_methodMap[$method]); + } + +/** + * Check if any loaded behavior implements the named finder. + * + * Will return true if any behavior provides a public method with + * the chosen name. + * + * @param string $method The method to check for. + * @return boolean + */ + public function hasFinder($method) { + $method = strtolower($method); + return isset($this->_finderMap[$method]); + } + +/** + * Invoke a method or finder on a behavior. + * + * @param string $method The method to invoke. + * @param array $args The arguments you want to invoke the method with. + * @return mixed The return value depends on the underlying behavior method. + * @throws Cake\Error\Exception When the method is unknown. + */ + public function call($method, array $args = []) { + $method = strtolower($method); + if ($this->hasMethod($method)) { + $alias = $this->_methodMap[$method]; + return call_user_func_array([$this->_loaded[$alias], $method], $args); + } + + if ($this->hasFinder($method)) { + $alias = $this->_finderMap[$method]; + return call_user_func_array([$this->_loaded[$alias], $method], $args); + } + + throw new Error\Exception(__d('cake_dev', 'Cannot call "%s" it does not belong to any attached behaviors.', $method)); + } + +} diff --git a/Cake/ORM/Table.php b/Cake/ORM/Table.php index 3aba0dfaeb2..df9acd7ac9f 100644 --- a/Cake/ORM/Table.php +++ b/Cake/ORM/Table.php @@ -24,6 +24,7 @@ use Cake\ORM\Association\BelongsToMany; use Cake\ORM\Association\HasMany; use Cake\ORM\Association\HasOne; +use Cake\ORM\BehaviorRegistry; use Cake\ORM\Entity; use Cake\Utility\Inflector; @@ -32,7 +33,7 @@ * * Exposes methods for retrieving data out of it, and manages the associations * this table has to other tables. Multiple instances of this class can be created - * for the same database table with different aliases, this allows you to address + * for the same database table with different aliases, this allows you to address * your database structure in a richer and more expressive way. * * ### Retrieving data @@ -110,7 +111,7 @@ class Table { protected $_associations = []; /** - * EventManager for this model. + * EventManager for this table. * * All model/behavior callbacks will be dispatched on this manager. * @@ -118,6 +119,13 @@ class Table { */ protected $_eventManager; +/** + * BehaviorRegistry for this table + * + * @var Cake\ORM\BehaviorRegistry + */ + protected $_behaviors; + /** * The name of the class that represent a single row for this table * @@ -138,6 +146,7 @@ class Table { * - schema: A \Cake\Database\Schema\Table object or an array that can be * passed to it. * - eventManager: An instance of an event manager to use for internal events + * - behaviors: A BehaviorRegistry. Generally not used outside of tests. * * @param array config Lsit of options for this table * @return void @@ -158,11 +167,15 @@ public function __construct(array $config = []) { if (!empty($config['entityClass'])) { $this->entityClass($config['entityClass']); } - $eventManager = null; + $eventManager = $behaviors = null; if (!empty($config['eventManager'])) { $eventManager = $config['eventManager']; } + if (!empty($config['behaviors'])) { + $behaviors = $config['behaviors']; + } $this->_eventManager = $eventManager ?: new EventManager(); + $this->_behaviors = $behaviors ?: new BehaviorRegistry($this); $this->initialize($config); } @@ -360,6 +373,33 @@ public function entityClass($name = null) { return $this->_entityClass; } +/** + * Add a behavior. + * + * Adds a behavior to this table's behavior collection. Behaviors + * provide an easy way to create horizontally re-usable features + * that can provide trait like functionality, and allow for events + * to be listened to. + * + * Example: + * + * Load a behavior, with some settings. + * + * {{{ + * $this->addBehavior('Tree', ['parent' => 'parentId']); + * }}} + * + * Behaviors are generally loaded during Table::initialize(). + * + * @param string $name The name of the behavior. Can be a short class reference. + * @param array $options The options for the behavior to use. + * @return void + * @see Cake\ORM\Behavior + */ + public function addBehavior($name, $options = []) { + $this->_behaviors->load($name, $options); + } + /** * Returns a association objected configured for the specified alias if any * @@ -523,7 +563,7 @@ public function belongsToMany($associated, array $options = []) { public function find($type, $options = []) { $query = $this->_buildQuery(); $query->select()->applyOptions($options); - return $this->{'find' . ucfirst($type)}($query, $options); + return $this->callFinder($type, $query, $options); } /** @@ -863,21 +903,36 @@ protected function _update($entity, $data) { * @return \Cake\ORM\Query * @throws \BadMethodCallException */ - public function callFinder($type, Query $query = null, $options = []) { - if (!method_exists($this, 'find' . ucfirst($type))) { + public function callFinder($type, Query $query, $options = []) { + $finder = 'find' . ucfirst($type); + $behaviorFinder = ($this->_behaviors && $this->_behaviors->hasFinder($finder)); + $missingMethod = (!method_exists($this, $finder) && !$behaviorFinder); + if ($missingMethod) { throw new \BadMethodCallException( - __d('cake_dev', 'Unknown table method %s', $type) + __d('cake_dev', 'Unknown finder method "%s"', $finder) ); } - if ($query === null) { - return $this->find($type, $options); + if ($behaviorFinder) { + return $this->_behaviors->call($finder, [$query, $options]); } - return $this->{'find' . ucfirst($type)}($query, $options); + return $this->{$finder}($query, $options); } /** - * Magic method to be able to call scoped finders without the - * find prefix + * Magic method to be able to call scoped finders & behaviors + * without the find prefix. + * + * ## Finder delegation + * + * You can use this feature to invoke finder methods, without + * adding the 'find' prefix or preparing a query ahead of time. + * For example, if your Table provided a `findRecent` finder, you + * could call `$table->recent()` instead. + * + * ## Behavior delegation + * + * If your Table uses any behaviors you can call them as if + * they were on the table object. * * @param string $method name of the method to be invoked * @param array $args List of arguments passed to the function @@ -885,11 +940,18 @@ public function callFinder($type, Query $query = null, $options = []) { * @throws \BadMethodCallException */ public function __call($method, $args) { + if ($this->_behaviors && $this->_behaviors->hasMethod($method)) { + return $this->_behaviors->call($method, $args); + } + $query = null; if (isset($args[0]) && $args[0] instanceof Query) { $query = array_shift($args); } $options = array_shift($args) ?: []; + if ($query === null) { + return $this->find($method, $options); + } return $this->callFinder($method, $query, $options); } diff --git a/Cake/Test/TestApp/Model/Behavior/PersisterOneBehaviorBehavior.php b/Cake/Test/TestApp/Model/Behavior/DuplicateBehavior.php similarity index 62% rename from Cake/Test/TestApp/Model/Behavior/PersisterOneBehaviorBehavior.php rename to Cake/Test/TestApp/Model/Behavior/DuplicateBehavior.php index 15eb25d3f16..86654fa7f32 100644 --- a/Cake/Test/TestApp/Model/Behavior/PersisterOneBehaviorBehavior.php +++ b/Cake/Test/TestApp/Model/Behavior/DuplicateBehavior.php @@ -1,11 +1,5 @@ where(['slug' => 'test']); + return $query; + } + + public function findNoSlug(Query $query, $options = []) { + $query->where(['slug' => null]); + return $query; + } + + public function slugify($value) { + return Inflector::slug($value); + } -/** - * Behavior to allow for dynamic and atomic manipulation of a Model's associations used for a find call. Most useful for limiting - * the amount of associations and data returned. - * - */ -class TestPluginPersisterTwoBehavior extends ModelBehavior { } diff --git a/Cake/Test/TestApp/Plugin/TestPlugin/Model/Behavior/TestPluginPersisterOneBehavior.php b/Cake/Test/TestApp/Plugin/TestPlugin/Model/Behavior/PersisterOneBehavior.php similarity index 59% rename from Cake/Test/TestApp/Plugin/TestPlugin/Model/Behavior/TestPluginPersisterOneBehavior.php rename to Cake/Test/TestApp/Plugin/TestPlugin/Model/Behavior/PersisterOneBehavior.php index 084a8c3b917..b03386a18b4 100644 --- a/Cake/Test/TestApp/Plugin/TestPlugin/Model/Behavior/TestPluginPersisterOneBehavior.php +++ b/Cake/Test/TestApp/Plugin/TestPlugin/Model/Behavior/PersisterOneBehavior.php @@ -1,11 +1,5 @@ assertTrue(in_array('Router', $result)); $result = App::objects('Model/Behavior', null, false); - $this->assertContains('PersisterOneBehaviorBehavior', $result); + $this->assertContains('SluggableBehavior', $result); $result = App::objects('Controller/Component', null, false); $this->assertContains('AppleComponent', $result); @@ -174,7 +174,7 @@ public function testListObjectsInPlugin() { $this->assertTrue(in_array('TestPluginPost', $result)); $result = App::objects('TestPlugin.Model/Behavior'); - $this->assertTrue(in_array('TestPluginPersisterOneBehavior', $result)); + $this->assertTrue(in_array('PersisterOneBehavior', $result)); $result = App::objects('TestPlugin.View/Helper'); $expected = array('OtherHelperHelper', 'PluggedHelperHelper', 'TestPluginAppHelper'); diff --git a/Cake/Test/TestCase/ORM/BehaviorRegistryTest.php b/Cake/Test/TestCase/ORM/BehaviorRegistryTest.php new file mode 100644 index 00000000000..5ff87ead3ae --- /dev/null +++ b/Cake/Test/TestCase/ORM/BehaviorRegistryTest.php @@ -0,0 +1,199 @@ +Table = new Table(['table' => 'articles']); + $this->EventManager = $this->Table->getEventManager(); + $this->Behaviors = new BehaviorRegistry($this->Table); + Configure::write('App.namespace', 'TestApp'); + } + +/** + * tearDown + * + * @return void + */ + public function tearDown() { + Plugin::unload(); + unset($this->Table, $this->EventManager, $this->Behaviors); + parent::tearDown(); + } + +/** + * Test loading behaviors. + * + * @return void + */ + public function testLoad() { + Plugin::load('TestPlugin'); + $settings = ['alias' => 'Sluggable', 'replacement' => '-']; + $result = $this->Behaviors->load('Sluggable', $settings); + $this->assertInstanceOf('TestApp\Model\Behavior\SluggableBehavior', $result); + $this->assertEquals($settings, $result->settings()); + + $result = $this->Behaviors->load('TestPlugin.PersisterOne'); + $this->assertInstanceOf('TestPlugin\Model\Behavior\PersisterOneBehavior', $result); + } + +/** + * Test load() binding listeners. + * + * @return void + */ + public function testLoadBindEvents() { + $result = $this->EventManager->listeners('Model.beforeFind'); + $this->assertCount(0, $result); + + $this->Behaviors->load('Sluggable'); + $result = $this->EventManager->listeners('Model.beforeFind'); + $this->assertCount(1, $result); + $this->assertInstanceOf('TestApp\Model\Behavior\SluggableBehavior', $result[0]['callable'][0]); + $this->assertEquals('beforeFind', $result[0]['callable'][1], 'Method name should match.'); + } + +/** + * Test load() with enabled = false + * + * @return void + */ + public function testLoadEnabledFalse() { + $result = $this->EventManager->listeners('Model.beforeFind'); + $this->assertCount(0, $result); + + $this->Behaviors->load('Sluggable', ['enabled' => false]); + $result = $this->EventManager->listeners('Model.beforeFind'); + $this->assertCount(0, $result); + } + +/** + * Test loading plugin behaviors + * + * @return void + */ + public function testLoadPlugin() { + Plugin::load('TestPlugin'); + $result = $this->Behaviors->load('TestPlugin.PersisterOne'); + $this->assertInstanceOf('TestPlugin\Model\Behavior\PersisterOneBehavior', $result); + } + +/** + * Test load() on undefined class + * + * @expectedException Cake\Error\MissingBehaviorException + * @return void + */ + public function testLoadMissingClass() { + $this->Behaviors->load('DoesNotExist'); + } + +/** + * Test load() duplicate method error + * + * @expectedException Cake\Error\Exception + * @expectedExceptionMessage TestApp\Model\Behavior\DuplicateBehavior contains duplicate method "slugify" + * @return void + */ + public function testLoadDuplicateMethodError() { + $this->Behaviors->load('Sluggable'); + $this->Behaviors->load('Duplicate'); + } + +/** + * test hasMethod() + * + * @return void + */ + public function testHasMethod() { + Plugin::load('TestPlugin'); + $this->Behaviors->load('TestPlugin.PersisterOne'); + $this->Behaviors->load('Sluggable'); + + $this->assertTrue($this->Behaviors->hasMethod('slugify')); + $this->assertTrue($this->Behaviors->hasMethod('SLUGIFY')); + + $this->assertTrue($this->Behaviors->hasMethod('persist')); + $this->assertTrue($this->Behaviors->hasMethod('PERSIST')); + + $this->assertFalse($this->Behaviors->hasMethod('__construct')); + $this->assertFalse($this->Behaviors->hasMethod('settings')); + $this->assertFalse($this->Behaviors->hasMethod('implementedEvents')); + + $this->assertFalse($this->Behaviors->hasMethod('nope')); + $this->assertFalse($this->Behaviors->hasMethod('beforeFind')); + $this->assertFalse($this->Behaviors->hasMethod('findNoSlug')); + } + +/** + * Test hasFinder() method. + * + * @return void + */ + public function testHasFinder() { + $this->Behaviors->load('Sluggable'); + + $this->assertTrue($this->Behaviors->hasFinder('findNoSlug')); + $this->assertTrue($this->Behaviors->hasFinder('findnoslug')); + $this->assertTrue($this->Behaviors->hasFinder('FINDNOSLUG')); + + $this->assertFalse($this->Behaviors->hasFinder('slugify')); + $this->assertFalse($this->Behaviors->hasFinder('beforeFind')); + $this->assertFalse($this->Behaviors->hasFinder('nope')); + } + +/** + * test call + * + * @return void + */ + public function testCall() { + $this->Behaviors->load('Sluggable'); + $result = $this->Behaviors->call('slugify', ['some value']); + $this->assertEquals('some_value', $result); + + $query = $this->getMock('Cake\ORM\Query', [], [null, null]); + $result = $this->Behaviors->call('findNoSlug', [$query]); + $this->assertEquals($query, $result); + } + +/** + * Test errors on unknown methods. + * + * @expectedException Cake\Error\Exception + * @expectedExceptionMessage Cannot call "nope" + */ + public function testCallError() { + $this->Behaviors->load('Sluggable'); + $this->Behaviors->call('nope'); + } + +} diff --git a/Cake/Test/TestCase/ORM/BehaviorTest.php b/Cake/Test/TestCase/ORM/BehaviorTest.php new file mode 100644 index 00000000000..b58f6261ebb --- /dev/null +++ b/Cake/Test/TestCase/ORM/BehaviorTest.php @@ -0,0 +1,90 @@ +getMock('Cake\ORM\Table'); + $settings = ['key' => 'value']; + $behavior = new TestBehavior($table, $settings); + $this->assertEquals($settings, $behavior->settings()); + } + +/** + * Test the default behavior of implementedEvents + * + * @return void + */ + public function testImplementedEvents() { + $table = $this->getMock('Cake\ORM\Table'); + $behavior = new TestBehavior($table); + $expected = [ + 'Model.beforeFind' => 'beforeFind' + ]; + $this->assertEquals($expected, $behavior->implementedEvents()); + } + +/** + * Test that implementedEvents uses the priority setting. + * + * @return void + */ + public function testImplementedEventsWithPriority() { + $table = $this->getMock('Cake\ORM\Table'); + $behavior = new TestBehavior($table, ['priority' => 10]); + $expected = [ + 'Model.beforeFind' => [ + 'priority' => 10, + 'callable' => 'beforeFind' + ] + ]; + $this->assertEquals($expected, $behavior->implementedEvents()); + } + +} diff --git a/Cake/Test/TestCase/ORM/TableTest.php b/Cake/Test/TestCase/ORM/TableTest.php index b606ba73d6d..06effe6a491 100644 --- a/Cake/Test/TestCase/ORM/TableTest.php +++ b/Cake/Test/TestCase/ORM/TableTest.php @@ -957,6 +957,63 @@ public function testExists() { $this->assertTrue($table->exists(['id' => 3, 'username' => 'larry'])); } +/** + * Test adding a behavior to a table. + * + * @return void + */ + public function testAddBehavior() { + $mock = $this->getMock('Cake\ORM\BehaviorRegistry', [], [], '', false); + $mock->expects($this->once()) + ->method('load') + ->with('Sluggable'); + + $table = new Table([ + 'table' => 'articles', + 'behaviors' => $mock + ]); + $table->addBehavior('Sluggable'); + } + +/** + * Ensure exceptions are raised on missing behaviors. + * + * @expectedException Cake\Error\MissingBehaviorException + */ + public function testAddBehaviorMissing() { + $table = TableRegistry::get('article'); + $this->assertNull($table->addBehavior('NopeNotThere')); + } + +/** + * Test mixin methods from behaviors. + * + * @return void + */ + public function testCallBehaviorMethod() { + $table = TableRegistry::get('article'); + $table->addBehavior('Sluggable'); + $this->assertEquals('some_value', $table->slugify('some value')); + } + +/** + * Test finder methods from behaviors. + * + * @return void + */ + public function testCallBehaviorFinder() { + $table = TableRegistry::get('article'); + $table->addBehavior('Sluggable'); + + $query = $table->noSlug(); + $this->assertInstanceOf('Cake\ORM\Query', $query); + $this->assertNotEmpty($query->clause('where')); + + $query = $table->find('noSlug'); + $this->assertInstanceOf('Cake\ORM\Query', $query); + $this->assertNotEmpty($query->clause('where')); + } + /** * Tests that it is possible to insert a new row using the save method * diff --git a/Cake/Utility/ObjectRegistry.php b/Cake/Utility/ObjectRegistry.php index dd0cd53c73d..45c2b995bb0 100644 --- a/Cake/Utility/ObjectRegistry.php +++ b/Cake/Utility/ObjectRegistry.php @@ -75,7 +75,7 @@ public function load($objectName, $settings = []) { if (!$className) { $this->_throwMissingClassError($objectName, substr($plugin, 0, -1)); } - $instance = $this->_create($className, $settings); + $instance = $this->_create($className, $name, $settings); $this->_loaded[$name] = $instance; return $instance; } @@ -104,10 +104,11 @@ abstract protected function _throwMissingClassError($class, $plugin); * required. * * @param string $class The class to build. + * @param string $alias The alias of the object. * @param array $settings The settings for construction * @return mixed */ - abstract protected function _create($class, $settings); + abstract protected function _create($class, $alias, $settings); /** * Get the loaded object list, or get the object instance at a given name. diff --git a/Cake/View/HelperRegistry.php b/Cake/View/HelperRegistry.php index 0e90a3c335b..ddef6c2bf90 100644 --- a/Cake/View/HelperRegistry.php +++ b/Cake/View/HelperRegistry.php @@ -133,11 +133,12 @@ protected function _throwMissingClassError($class, $plugin) { * Part of the template method for Cake\Utility\ObjectRegistry::load() * Enabled helpers will be registered with the event manager. * - * @param string $class The classname that is missing. + * @param string $class The class to create. + * @param string $alias The alias of the loaded helper. * @param array $settings An array of settings to use for the helper. * @return Component The constructed helper class. */ - protected function _create($class, $settings) { + protected function _create($class, $alias, $settings) { $instance = new $class($this->_View, $settings); $vars = array('request', 'theme', 'plugin'); foreach ($vars as $var) {