Skip to content

Commit

Permalink
Integrate behavior finders and behavior delegation into Table.
Browse files Browse the repository at this point in the history
Add behavior finder method & behavior method delegation into Table. It
looks and works very similar to how it did in 2.x but with new classes
and I think a simpler design as pattern based methods have been removed.
  • Loading branch information
markstory committed Oct 25, 2013
1 parent 02ebe1e commit 23f721f
Show file tree
Hide file tree
Showing 2 changed files with 129 additions and 12 deletions.
84 changes: 72 additions & 12 deletions Cake/ORM/Table.php
Expand Up @@ -23,6 +23,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;

Expand All @@ -31,7 +32,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
Expand Down Expand Up @@ -109,14 +110,21 @@ class Table {
protected $_associations = [];

/**
* EventManager for this model.
* EventManager for this table.
*
* All model/behavior callbacks will be dispatched on this manager.
*
* @var Cake\Event\EventManager
*/
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
*
Expand All @@ -137,6 +145,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
Expand All @@ -157,11 +166,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);
}

Expand Down Expand Up @@ -359,6 +372,31 @@ 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']);
* }}}
*
* @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 null
* @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
*
Expand Down Expand Up @@ -522,7 +560,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);
}

/**
Expand Down Expand Up @@ -709,33 +747,55 @@ public function save(Entity $entity, $options = []) {
* @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
* @return mixed
* @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);
}

Expand Down
57 changes: 57 additions & 0 deletions Cake/Test/TestCase/ORM/TableTest.php
Expand Up @@ -887,6 +887,63 @@ public function testReciprocalBelongsToMany() {
$this->assertInstanceOf('TestApp\Model\Entity\ArticlesTag', $result->tags[0]->extraInfo);
}

/**
* 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
*
Expand Down

0 comments on commit 23f721f

Please sign in to comment.