Browse Source

Add getByProperty and refactor association handling in EntityContext

Add a way to get associations by property name. This allows other parts
of the framework to operate on associations knowing only the property
names.
mark_story 12 years ago
parent
commit
1532032f6e

+ 15 - 0
src/ORM/Associations.php

@@ -63,6 +63,21 @@ class Associations {
 	}
 
 /**
+ * Fetch an association by property name.
+ *
+ * @param string $prop The property to find an association by.
+ * @return Association|null Either the association or null.
+ */
+	public function getByProperty($prop) {
+		foreach ($this->_items as $assoc) {
+			if ($assoc->property() === $prop) {
+				return $assoc;
+			}
+		}
+		return null;
+	}
+
+/**
  * Check for an attached association by name.
  *
  * @param string $alias The association alias to get.

+ 61 - 47
src/View/Form/EntityContext.php

@@ -17,6 +17,7 @@ namespace Cake\View\Form;
 use Cake\Network\Request;
 use Cake\ORM\Entity;
 use Cake\ORM\TableRegistry;
+use Cake\Utility\Inflector;
 use Cake\Validation\Validator;
 use Traversable;
 
@@ -33,11 +34,10 @@ use Traversable;
  *   from, an array of table instances in the case of an form spanning
  *   multiple entities, or the name(s) of the table.
  *   If this is null the table name(s) will be determined using conventions.
- *   This table object will be used to fetch the schema and
- *   validation information.
  * - `validator` Either the Validation\Validator to use, or the name of the
  *   validation method to call on the table object. For example 'default'.
- *   Defaults to 'default'.
+ *   Defaults to 'default'. Can be an array of table alias=>validators when
+ *   dealing with associated forms.
  */
 class EntityContext {
 
@@ -56,19 +56,11 @@ class EntityContext {
 	protected $_context;
 
 /**
- * The plural name of the top level entity/table object.
+ * The name of the top level entity/table object.
  *
  * @var string
  */
-	protected $_pluralName;
-
-/**
- * A dictionary of validators and their
- * related tables.
- *
- * @var array
- */
-	protected $_validators = [];
+	protected $_rootName;
 
 /**
  * A dictionary of tables
@@ -87,9 +79,8 @@ class EntityContext {
 		$this->_request = $request;
 		$context += [
 			'entity' => null,
-			'schema' => null,
 			'table' => null,
-			'validator' => null
+			'validator' => [],
 		];
 		$this->_context = $context;
 		$this->_prepare();
@@ -101,21 +92,12 @@ class EntityContext {
  * @return void
  */
 	protected function _prepare() {
-		$table = null;
-		// TODO handle the other cases (string, array, instance)
-		if (is_string($this->_context['table'])) {
-			$plural = $this->_context['table'];
-		}
-		$table = TableRegistry::get($plural);
-
-		if (is_object($this->_context['validator'])) {
-			$this->_validators['_default'] = $this->_context['validator'];
-		} elseif (is_string($this->_context['validator'])) {
-			$this->_validators['_default'] = $table->validator($this->_context['validator']);
+		$table = $this->_context['table'];
+		if (is_string($table)) {
+			$table = TableRegistry::get($table);
 		}
-
-		$this->_pluralName = $plural;
-		$this->_tables[$plural] = $table;
+		$alias = $this->_rootName = $table->alias();
+		$this->_tables[$alias] = $table;
 	}
 
 /**
@@ -131,7 +113,7 @@ class EntityContext {
 			return null;
 		}
 		$parts = explode('.', $field);
-		$entity = $this->_getEntity($parts);
+		list($entity, $prop) = $this->_getEntity($parts);
 		if (!$entity) {
 			return null;
 		}
@@ -146,19 +128,20 @@ class EntityContext {
  * will be returned.
  *
  * @param array $path The path to traverse to find the leaf entity.
- * @return boolean|Entity Either the leaf entity or false.
+ * @return array
  */
 	protected function _getEntity($path) {
 		$entity = $this->_context['entity'];
 		if (count($path) === 1) {
-			return $entity;
+			return [$entity, $this->_rootName];
 		}
 
 		// Remove the Table name if present.
-		if (count($path) > 1 && $path[0] === $this->_pluralName) {
+		if (count($path) > 1 && $path[0] === $this->_rootName) {
 			array_shift($path);
 		}
 
+		$lastProp = $this->_rootName;
 		foreach ($path as $prop) {
 			$next = $this->_getProp($entity, $prop);
 			if (
@@ -166,11 +149,14 @@ class EntityContext {
 				!($next instanceof Traversable) &&
 				!($next instanceof Entity)
 			) {
-				return $entity;
+				return [$entity, $lastProp];
+			}
+			if (!is_numeric($prop)) {
+				$lastProp = $prop;
 			}
 			$entity = $next;
 		}
-		return false;
+		return [false, false];
 	}
 
 /**
@@ -191,7 +177,6 @@ class EntityContext {
 		return $target->get($field);
 	}
 
-
 /**
  * Check if a field should be marked as required.
  *
@@ -203,13 +188,17 @@ class EntityContext {
 			return false;
 		}
 		$parts = explode('.', $field);
-		$entity = $this->_getEntity($parts);
+		list($entity, $prop) = $this->_getEntity($parts);
 		if (!$entity) {
 			return false;
 		}
 
+		$validator = $this->_getValidator($prop);
+		if (!$validator) {
+			return false;
+		}
+
 		$field = array_pop($parts);
-		$validator = $this->_getValidator($entity);
 		if (!$validator->hasField($field)) {
 			return false;
 		}
@@ -221,19 +210,44 @@ class EntityContext {
  * Get the validator associated to an entity based on naming
  * conventions.
  *
- * If no match is found the `_root` validator will be used.
- *
- * @param Cake\ORM\Entity $entity
- * @return Validator
+ * @param string $entity The entity name to get a validator for.
+ * @return Validator|false
  */
 	protected function _getValidator($entity) {
-		list($ns, $entityClass) = namespaceSplit(get_class($entity));
-		if (isset($this->_validators[$entityClass])) {
-			return $this->_validators[$entityClass];
+		$table = $this->_getTable($entity);
+		$alias = $table->alias();
+
+		$method = 'default';
+		if (is_string($this->_context['validator'])) {
+			$method = $this->_context['validator'];
+		} elseif (isset($this->_context['validator'][$alias])){
+			$method = $this->_context['validator'][$alias];
 		}
-		if (isset($this->_validators['_default'])) {
-			return $this->_validators['_default'];
+		return $table->validator($method);
+	}
+
+/**
+ * Get the table instance
+ *
+ * @param string $prop The property name to get a table for.
+ * @return Cake\ORM\Table The table instance.
+ */
+	protected function _getTable($prop) {
+		if (isset($this->_tables[$prop])) {
+			return $this->_tables[$prop];
 		}
+		$root = $this->_tables[$this->_rootName];
+		$assoc = $root->associations()->getByProperty($prop);
+
+		// No assoc, use the default table to prevent
+		// downstream failures.
+		if (!$assoc) {
+			return $root;
+		}
+
+		$target = $assoc->target();
+		$this->_tables[$prop] = $target;
+		return $target;
 	}
 
 	public function type($field) {

+ 14 - 0
tests/TestCase/ORM/AssociationsTest.php

@@ -63,6 +63,20 @@ class AssociationsTest extends TestCase {
 	}
 
 /**
+ * Test getting associations by property.
+ *
+ * @return void
+ */
+	public function testGetByProperty() {
+		$belongsTo = new BelongsTo('Users', []);
+		$this->assertEquals('user', $belongsTo->property());
+		$this->associations->add('Users', $belongsTo);
+		$this->assertNull($this->associations->get('user'));
+
+		$this->assertSame($belongsTo, $this->associations->getByProperty('user'));
+	}
+
+/**
  * Test associations with plugin names.
  *
  * @return void

+ 108 - 17
tests/TestCase/View/Form/EntityContextTest.php

@@ -19,6 +19,7 @@ use Cake\ORM\Table;
 use Cake\ORM\TableRegistry;
 use Cake\Network\Request;
 use Cake\TestSuite\TestCase;
+use Cake\Validation\Validator;
 use Cake\View\Form\EntityContext;
 
 /**
@@ -124,11 +125,11 @@ class EntityContextTest extends TestCase {
 	}
 
 /**
- * Test isRequired in basic scenarios.
+ * Test validator as a string.
  *
  * @return void
  */
-	public function testIsRequired() {
+	public function testIsRequiredStringValidator() {
 		$articles = TableRegistry::get('Articles');
 
 		$validator = $articles->validator();
@@ -142,7 +143,7 @@ class EntityContextTest extends TestCase {
 		$context = new EntityContext($this->request, [
 			'entity' => new Entity(),
 			'table' => 'Articles',
-			'validator' => $validator
+			'validator' => 'default',
 		]);
 
 		$this->assertTrue($context->isRequired('Articles.title'));
@@ -155,39 +156,129 @@ class EntityContextTest extends TestCase {
 	}
 
 /**
- * Test validator as a string.
+ * Test isRequired on associated entities.
  *
  * @return void
  */
-	public function testIsRequiredStringValidator() {
+	public function testIsRequiredAssociatedHasMany() {
 		$articles = TableRegistry::get('Articles');
+		$articles->hasMany('Comments');
+		$comments = TableRegistry::get('Comments');
 
 		$validator = $articles->validator();
 		$validator->add('title', 'minlength', [
 			'rule' => ['minlength', 10]
-		])
-		->add('body', 'maxlength', [
-			'rule' => ['maxlength', 1000]
-		])->allowEmpty('body');
+		]);
 
+		$validator = $comments->validator();
+		$validator->add('comment', 'length', [
+			'rule' => ['minlength', 10]
+		]);
+
+		$row = new Entity([
+			'title' => 'My title',
+			'comments' => [
+				new Entity(['comment' => 'First comment']),
+				new Entity(['comment' => 'Second comment']),
+			]
+		]);
 		$context = new EntityContext($this->request, [
-			'entity' => new Entity(),
+			'entity' => $row,
 			'table' => 'Articles',
 			'validator' => 'default',
 		]);
 
-		$this->assertTrue($context->isRequired('Articles.title'));
+		// $this->assertTrue($context->isRequired('Articles.title'));
+		// $this->assertFalse($context->isRequired('Articles.body'));
+
+		$this->assertTrue($context->isRequired('comments.0.comment'));
+		$this->assertTrue($context->isRequired('Articles.comments.0.comment'));
+
+		$this->assertFalse($context->isRequired('comments.0.other'));
+		$this->assertFalse($context->isRequired('Articles.comments.0.other'));
+	}
+
+/**
+ * Test isRequired on associated entities with custom validators.
+ *
+ * @return void
+ */
+	public function testIsRequiredAssociatedValidator() {
+		$articles = TableRegistry::get('Articles');
+		$articles->hasMany('Comments');
+		$comments = TableRegistry::get('Comments');
+
+		$validator = new Validator();
+		$validator->add('title', 'minlength', [
+			'rule' => ['minlength', 10]
+		]);
+		$articles->validator('create', $validator);
+
+		$validator = new Validator();
+		$validator->add('comment', 'length', [
+			'rule' => ['minlength', 10]
+		]);
+		$comments->validator('custom', $validator);
+
+		$row = new Entity([
+			'title' => 'My title',
+			'comments' => [
+				new Entity(['comment' => 'First comment']),
+				new Entity(['comment' => 'Second comment']),
+			]
+		]);
+		$context = new EntityContext($this->request, [
+			'entity' => $row,
+			'table' => 'Articles',
+			'validator' => [
+				'Articles' => 'create',
+				'Comments' => 'custom'
+			]
+		]);
+
 		$this->assertTrue($context->isRequired('title'));
-		$this->assertFalse($context->isRequired('Articles.body'));
 		$this->assertFalse($context->isRequired('body'));
+		$this->assertTrue($context->isRequired('comments.0.comment'));
+		$this->assertTrue($context->isRequired('comments.1.comment'));
 	}
 
-	public function testIsRequiredAssociated() {
-		$this->markTestIncomplete();
-	}
+/**
+ * Test isRequired on associated entities.
+ *
+ * @return void
+ */
+	public function testIsRequiredAssociatedBelongsTo() {
+		$articles = TableRegistry::get('Articles');
+		$articles->belongsTo('Users');
+		$users = TableRegistry::get('Users');
 
-	public function testIsRequiredAssociatedValidator() {
-		$this->markTestIncomplete();
+		$validator = new Validator();
+		$validator->add('title', 'minlength', [
+			'rule' => ['minlength', 10]
+		]);
+		$articles->validator('create', $validator);
+
+		$validator = new Validator();
+		$validator->add('username', 'length', [
+			'rule' => ['minlength', 10]
+		]);
+		$users->validator('custom', $validator);
+
+		$row = new Entity([
+			'title' => 'My title',
+			'user' => new Entity(['username' => 'Mark']),
+		]);
+		$context = new EntityContext($this->request, [
+			'entity' => $row,
+			'table' => 'Articles',
+			'validator' => [
+				'Articles' => 'create',
+				'Users' => 'custom'
+			]
+		]);
+
+		$this->assertTrue($context->isRequired('user.username'));
+		$this->assertFalse($context->isRequired('user.first_name'));
 	}
 
 }