Skip to content
Browse files

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.
  • Loading branch information...
1 parent 5cdef0d commit 1532032f6e7834625b06c6ef5cd5cb48ab22fc8e @markstory markstory committed
View
15 src/ORM/Associations.php
@@ -63,6 +63,21 @@ public function get($alias) {
}
/**
+ * 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.
View
108 src/View/Form/EntityContext.php
@@ -17,6 +17,7 @@
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 @@
* 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 @@ public function __construct(Request $request, array $context) {
$this->_request = $request;
$context += [
'entity' => null,
- 'schema' => null,
'table' => null,
- 'validator' => null
+ 'validator' => [],
];
$this->_context = $context;
$this->_prepare();
@@ -101,21 +92,12 @@ public function __construct(Request $request, array $context) {
* @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 @@ public function val($field) {
return null;
}
$parts = explode('.', $field);
- $entity = $this->_getEntity($parts);
+ list($entity, $prop) = $this->_getEntity($parts);
if (!$entity) {
return null;
}
@@ -146,19 +128,20 @@ public function val($field) {
* 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 @@ protected function _getEntity($path) {
!($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 @@ protected function _getProp($target, $field) {
return $target->get($field);
}
-
/**
* Check if a field should be marked as required.
*
@@ -203,13 +188,17 @@ public function isRequired($field) {
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 @@ public function isRequired($field) {
* 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) {
View
14 tests/TestCase/ORM/AssociationsTest.php
@@ -63,6 +63,20 @@ public function testAddHasRemoveAndGet() {
}
/**
+ * 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
View
125 tests/TestCase/View/Form/EntityContextTest.php
@@ -19,6 +19,7 @@
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 @@ public function testValAssociated() {
}
/**
- * 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 @@ public function testIsRequired() {
$context = new EntityContext($this->request, [
'entity' => new Entity(),
'table' => 'Articles',
- 'validator' => $validator
+ 'validator' => 'default',
]);
$this->assertTrue($context->isRequired('Articles.title'));
@@ -155,39 +156,129 @@ public function testIsRequired() {
}
/**
- * 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'));
}
}

0 comments on commit 1532032

Please sign in to comment.
Something went wrong with that request. Please try again.