Skip to content

Commit

Permalink
Merge pull request #15 from beowulfenator/master
Browse files Browse the repository at this point in the history
issue #8 and raw value getter
  • Loading branch information
voskobovich committed Oct 2, 2015
2 parents 500c78f + c170dd1 commit 75cd9a7
Show file tree
Hide file tree
Showing 6 changed files with 292 additions and 27 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Expand Up @@ -5,4 +5,7 @@ composer.lock
vendor
tests/_log/*
tests/_runtime/*
tests/unit/UnitTester.php
tests/unit/UnitTester.php
# sublime
*.sublime-project
*.sublime-workspace
109 changes: 93 additions & 16 deletions ManyToManyBehavior.php
Expand Up @@ -28,6 +28,13 @@ class ManyToManyBehavior extends \yii\base\Behavior
*/
private $_values = [];

/**
* Used to store fields that this behavior creates. Each field refers to a relation
* and has optional getters and setters.
* @var array
*/
private $_fields = [];

/**
* Events list
* @return array
Expand All @@ -40,6 +47,48 @@ public function events()
];
}

/**
* Invokes init of parent class and assigns proper values to internal _fields variable
*/
public function init()
{
parent::init();

//configure _fields
foreach ($this->relations as $attributeName => $params) {
//add primary field
$this->_fields[$attributeName] = [
'attribute' => $attributeName,
];
if (isset($params['get'])) {
$this->_fields[$attributeName]['get'] = $params['get'];
}
if (isset($params['set'])) {
$this->_fields[$attributeName]['set'] = $params['set'];
}

//add secondary fields
if (isset($params['fields'])) {
foreach ($params['fields'] as $fieldName => $params) {
$fullFieldName = $attributeName.'_'.$fieldName;
if (isset($this->_fields[$fullFieldName])) {
throw new ErrorException("Ambiguous field name definition: {$fullFieldName}");
}

$this->_fields[$fullFieldName] = [
'attribute' => $attributeName,
];
if (isset($params['get'])) {
$this->_fields[$fullFieldName]['get'] = $params['get'];
}
if (isset($params['set'])) {
$this->_fields[$fullFieldName]['set'] = $params['set'];
}
}
}
}
}

/**
* Save all dirty (changed) relation values ($this->_values) to the database
* @param $event
Expand Down Expand Up @@ -74,8 +123,17 @@ public function saveRelations($event)
// many-to-many
if (!empty($relation->via) && $relation->multiple) {
//Assuming junction column is visible from the primary model connection
list($junctionTable) = array_values($relation->via->from);
list($junctionColumn) = array_keys($relation->via->link);
if (is_array($relation->via)) {
//via()
$via = $relation->via[1];
$junctionModelClass = $via->modelClass;
$junctionTable = $junctionModelClass::tableName();
list($junctionColumn) = array_keys($via->link);
} else {
//viaTable()
list($junctionTable) = array_values($relation->via->from);
list($junctionColumn) = array_keys($relation->via->link);
}
list($relatedColumn) = array_values($relation->link);

$connection = $primaryModel::getDb();
Expand Down Expand Up @@ -200,7 +258,8 @@ private function getNewValue($attributeName)
* @param $attributeName
* @return mixed
*/
private function getDefaultValue($attributeName) {
private function getDefaultValue($attributeName)
{
$relationParams = $this->getRelationParams($attributeName);
if (!isset($relationParams['default'])) {
return null;
Expand Down Expand Up @@ -241,6 +300,21 @@ private function getViaTableParams($attributeName)
return isset($params['viaTableValues']) ? $params['viaTableValues'] : [];
}

/**
* Get parameters of a field
* @param $fieldName
* @return mixed
* @throws ErrorException
*/
private function getFieldParams($fieldName)
{
if (empty($this->_fields[$fieldName])) {
throw new ErrorException("Parameter \"{$fieldName}\" does not exist");
}

return $this->_fields[$fieldName];
}

/**
* Get parameters of a relation
* @param $attributeName
Expand Down Expand Up @@ -287,7 +361,7 @@ private function getRelationName($attributeName)
*/
public function canGetProperty($name, $checkVars = true)
{
return array_key_exists($name, $this->relations) ?
return array_key_exists($name, $this->_fields) ?
true : parent::canGetProperty($name, $checkVars);
}

Expand All @@ -305,7 +379,7 @@ public function canGetProperty($name, $checkVars = true)
*/
public function canSetProperty($name, $checkVars = true, $checkBehaviors = true)
{
return array_key_exists($name, $this->relations) ?
return array_key_exists($name, $this->_fields) ?
true : parent::canSetProperty($name, $checkVars, $checkBehaviors);
}

Expand All @@ -320,21 +394,23 @@ public function canSetProperty($name, $checkVars = true, $checkBehaviors = true)
*/
public function __get($name)
{
$relationName = $this->getRelationName($name);
$relationParams = $this->getRelationParams($name);
$fieldParams = $this->getFieldParams($name);
$attributeName = $fieldParams['attribute'];

$relationName = $this->getRelationName($attributeName);

if ($this->hasNewValue($name)) {
$value = $this->getNewValue($name);
if ($this->hasNewValue($attributeName)) {
$value = $this->getNewValue($attributeName);
} else {
$relation = $this->owner->getRelation($relationName);
$foreignModel = new $relation->modelClass();
$value = $relation->select($foreignModel->getPrimaryKey())->column();
}

if (!empty($relationParams['get'])) {
return $this->callUserFunction($relationParams['get'], $value);
} else {
if (empty($fieldParams['get'])) {
return $value;
} else {
return $this->callUserFunction($fieldParams['get'], $value);
}
}

Expand All @@ -347,12 +423,13 @@ public function __get($name)
*/
public function __set($name, $value)
{
$relationParams = $this->getRelationParams($name);
$fieldParams = $this->getFieldParams($name);
$attributeName = $fieldParams['attribute'];

if (!empty($relationParams['set'])) {
$this->_values[$name] = $this->callUserFunction($relationParams['set'], $value);
if (!empty($fieldParams['set'])) {
$this->_values[$attributeName] = $this->callUserFunction($fieldParams['set'], $value);
} else {
$this->_values[$name] = $value;
$this->_values[$attributeName] = $value;
}
}
}
46 changes: 36 additions & 10 deletions README.md
Expand Up @@ -44,22 +44,48 @@ Relation names don't need to end in `_list`, and you can use any name for a rela

### Custom getters and setters ###

Attributes lik `author_list` and `review_list` in the `Book` model are created automatically. By default, they are configured to accept data from a standard select input (see below). However, it is possible to use custom getter and setter functions, which may be useful for interaction with more complex frontend scripts:
Attributes like `author_list` and `review_list` in the `Book` model are created automatically. By default, they are configured to accept data from a standard select input (see below). However, it is possible to use custom getter and setter functions, which may be useful for interaction with more complex frontend scripts. It is possible to define many alternative getters and setters for a given attribute:

```php
...
//...
'author_list' => [
'authors',
'get' => function($value) {
return JSON::encode($value);
},
'set' => function($value) {
return JSON::decode($value);
},
'fields' => [
'json' => [
'get' => function($value) {
//from internal representation (array) to user type
return JSON::encode($value);
},
'set' => function($value) {
//from user type to internal representation (array)
return JSON::decode($value);
},
],
'as_string' => [
'get' => function($value) {
//from internal representation (array) to user type
return implode(',', $value);
},
'set' => function($value) {
//from user type to internal representation (array)
return explode(',', $value);
},
],
],
]
...
//...
```

Field name is concatenated to the attribute name with an underscore. In this example, accessing `$model->authors` will result in an array of IDs, `$model->authors_json` will return a JSON string and `$model->authors_as_string` will return a comma-separated string of IDs. Setters work similarly.

Getters and setters may be ommitted to fall back to default behavior (arrays of IDs).

###### NOTE ######
The setter function receives whatever data comes through the `$_REQUEST` and is expected to return the array of the related model IDs. The getter function receives the array of the related model IDs.

###### COMPATIBILITY NOTE ######
Specifying getters and setters for the primary attribute (`author_list` in the above example) is still supported, but not recommended. Best practice is to use primary attribute to get and set values as array of IDs and create `fields` to use other getters and setters.

### Custom junction table values ###

For seting additional values in junction table (apart columns required for relation), you can use `viaTableValues`:
Expand Down Expand Up @@ -125,7 +151,7 @@ function($model, $relationName, $attributeName) {

### Applying the behaviour several times to a single relationship ###

It is possible to use this behavior for a single relationship multiple times in a single model. For example, it is possible to have `author_list` for normal form input and `author_list_json` for JSON string input at the same time. However, one should keep in mind that parameters are processed in the order they are given in the config, so if both `author_list` and `author_list_json` contain data, items from `author_list` will be saved first only to be overwritten by items from `author_list_json` afterwards. It is advised to provide data to only one of those attributes to avoid this.
It is possible to use this behavior for a single relationship multiple times in a single model. This is not recommended, however.


Adding validation rules
Expand Down
49 changes: 49 additions & 0 deletions tests/_data/BookBadFields.php
@@ -0,0 +1,49 @@
<?php

namespace data;

use Yii;
use yii\helpers\JSON;

class BookBadFields extends Book
{

public function behaviors()
{
return
[
[
'class' => \voskobovich\behaviors\ManyToManyBehavior::className(),
'relations' => [
'author' => [
'authors',
'fields' => [
'list_json' => [
'get' => function($value) {
return JSON::encode($value);
},
'set' => function($value) {
return JSON::decode($value);
},
],
],
],
'author_list' => [
'reviews',
'fields' => [
'json' => [
'get' => function($value) {
return JSON::encode($value);
},
'set' => function($value) {
return JSON::decode($value);
},
],
],
],
],
],
];
}

}
57 changes: 57 additions & 0 deletions tests/_data/BookJsonFields.php
@@ -0,0 +1,57 @@
<?php

namespace data;

use Yii;
use yii\helpers\JSON;

class BookJsonFields extends Book
{

public function behaviors()
{
return
[
[
'class' => \voskobovich\behaviors\ManyToManyBehavior::className(),
'relations' => [
'author_list' => [
'authors',
'fields' => [
'json' => [
'get' => function($value) {
return JSON::encode($value);
},
'set' => function($value) {
return JSON::decode($value);
},
],
],
],
'review_list' => [
'reviews',
'fields' => [
'json' => [
'get' => function($value) {
return JSON::encode($value);
},
'set' => function($value) {
return JSON::decode($value);
},
],
'implode' => [
'get' => function($value) {
return implode(',', $value);
},
'set' => function($value) {
return explode(',', $value);
},
],
],
],
],
],
];
}

}

0 comments on commit 75cd9a7

Please sign in to comment.