├── .gitignore ├── CHANGELOG.md ├── LICENCE.md ├── README.md ├── composer.json ├── phpunit.xml.dist ├── src └── CompositeForm.php └── tests ├── LoadActiveFormTest.php ├── LoadApiTest.php ├── TestCase.php ├── ValidateTest.php ├── _forms ├── MetaForm.php ├── OnlyNestedProductForm.php ├── ProductForm.php └── ValueForm.php └── bootstrap.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /composer.lock 3 | /phpunit.xml -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.1.0 4 | 5 | - Allowed loading of partial data: the `load($data)` method returns `true` if at least one form's data is presented in `$data` array (vjik) 6 | - Renamed `private $forms` variable to `protected $_forms` (vjik, makcumka2000) -------------------------------------------------------------------------------- /LICENCE.md: -------------------------------------------------------------------------------- 1 | # The BSD License (BSD) 2 | 3 | Copyright © 2016 by Dmitry Eliseev (ElisDN) All rights reserved. 4 | 5 | > Redistribution and use in source and binary forms, with or without modification, 6 | > are permitted provided that the following conditions are met: 7 | > 8 | > Redistributions of source code must retain the above copyright notice, this 9 | > list of conditions and the following disclaimer. 10 | > 11 | > Redistributions in binary form must reproduce the above copyright notice, this 12 | > list of conditions and the following disclaimer in the documentation and/or 13 | > other materials provided with the distribution. 14 | > 15 | > Neither the name of Dmitry Eliseev (ElisDN) nor the names of its 16 | > contributors may be used to endorse or promote products derived from 17 | > this software without specific prior written permission. 18 | > 19 | >THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 20 | >ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 21 | >WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | >DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 23 | >ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 24 | >(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | >LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 26 | >ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | >(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | >SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Composite Form for Yii2 Framework 2 | 3 | The extension allows to create nested form models. 4 | 5 | ## Installation 6 | 7 | Install with composer: 8 | 9 | ```bash 10 | composer require elisdn/yii2-composite-form 11 | ``` 12 | 13 | ## Usage samples 14 | 15 | Create any simple form model for SEO information: 16 | 17 | ```php 18 | class MetaForm extends Model 19 | { 20 | public $title; 21 | public $description; 22 | public $keywords; 23 | 24 | public function rules() 25 | { 26 | return [ 27 | [['title'], 'string', 'max' => 255], 28 | [['description', 'keywords'], 'string'], 29 | ]; 30 | } 31 | } 32 | ``` 33 | 34 | and for characteristics: 35 | 36 | ```php 37 | class ValueForm extends Model 38 | { 39 | public $value; 40 | 41 | private $_characteristic; 42 | 43 | public function __construct(Characteristic $characteristic, $config = []) 44 | { 45 | $this->_characteristic = $characteristic; 46 | parent::__construct($config); 47 | } 48 | 49 | public function rules() 50 | { 51 | return [ 52 | ['value', 'safe'], 53 | ]; 54 | } 55 | 56 | public function attributeLabels() 57 | { 58 | return [ 59 | 'value' => $this->_characteristic->name, 60 | ]; 61 | } 62 | 63 | public function getCharacteristicId() 64 | { 65 | return $this->_characteristic->id; 66 | } 67 | } 68 | ``` 69 | 70 | And create a composite form model which uses both as an internal forms: 71 | 72 | ```php 73 | use elisdn\compositeForm\CompositeForm; 74 | 75 | /** 76 | * @property MetaForm $meta 77 | * @property ValueForm[] $values 78 | */ 79 | class ProductCreateForm extends CompositeForm 80 | { 81 | public $code; 82 | public $name; 83 | 84 | public function __construct($config = []) 85 | { 86 | $this->meta = new MetaForm(); 87 | $this->values = array_map(function (Characteristic $characteristic) { 88 | return new ValueForm($characteristic); 89 | }, Characteristic::find()->orderBy('sort')->all()); 90 | parent::__construct($config); 91 | } 92 | 93 | public function rules() 94 | { 95 | return [ 96 | [['code', 'name'], 'required'], 97 | [['code', 'name'], 'string', 'max' => 255], 98 | [['code'], 'unique', 'targetClass' => Product::className()], 99 | ]; 100 | } 101 | 102 | protected function internalForms() 103 | { 104 | return ['meta', 'values']; 105 | } 106 | } 107 | ``` 108 | 109 | That is all. After all just use external `$form` and internal `$form->meta` and `$form->values` models for `ActiveForm`: 110 | 111 | ```php 112 | 113 | 114 |

Common

115 | 116 | field($model, 'code')->textInput() ?> 117 | field($model, 'name')->textInput() ?> 118 | 119 |

Characteristics

120 | 121 | values as $i => $valueForm): ?> 122 | field($valueForm, '[' . $i . ']value')->textInput() ?> 123 | 124 | 125 |

SEO

126 | 127 | field($model->meta, 'title')->textInput() ?> 128 | field($model->meta, 'description')->textarea(['rows' => 2]) ?> 129 | 130 |
131 | 'btn btn-success']) ?> 132 |
133 | 134 | 135 | ``` 136 | 137 | and for your application's services: 138 | 139 | ```php 140 | class ProductManageService 141 | { 142 | private $products; 143 | 144 | public function __construct(ProductRepository $products) 145 | { 146 | $this->products = $products; 147 | } 148 | 149 | public function create(ProductCreateForm $form) 150 | { 151 | $product = Product::create( 152 | $form->code, 153 | $form->name, 154 | new Meta( 155 | $form->meta->title, 156 | $form->meta->description, 157 | $form->meta->keywords 158 | ) 159 | ); 160 | 161 | foreach ($form->values as $valueForm) { 162 | $product->changeValue($valueForm->getCharacteristicId(), $valueForm->value); 163 | } 164 | 165 | $this->products->save($product); 166 | 167 | return $product->id; 168 | } 169 | 170 | ... 171 | } 172 | ``` 173 | 174 | with simple controller for web: 175 | 176 | ```php 177 | class ProductController extends \yii\web\Controller 178 | { 179 | ... 180 | 181 | public function actionCreate() 182 | { 183 | $form = new ProductCreateForm(); 184 | 185 | if ($form->load(Yii::$app->request->post()) && $form->validate()) { 186 | $id = $this->service->create($form); 187 | return $this->redirect(['view', 'id' => $id]); 188 | } 189 | 190 | return $this->render('create', [ 191 | 'model' => $form, 192 | ]); 193 | } 194 | } 195 | ``` 196 | 197 | or for API: 198 | 199 | ```php 200 | class ProductController extends \yii\rest\Controller 201 | { 202 | ... 203 | 204 | public function actionCreate() 205 | { 206 | $form = new ProductCreateForm(); 207 | $form->load(Yii::$app->request->getBodyParams()); 208 | 209 | if ($form->validate()) { 210 | $id = $this->service->create($form); 211 | $response = Yii::$app->getResponse(); 212 | $response->setStatusCode(201); 213 | $response->getHeaders()->set('Location', Url::to(['view', 'id' => $id], true)); 214 | return []; 215 | } 216 | 217 | return $form; 218 | } 219 | } 220 | ``` -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "elisdn/yii2-composite-form", 3 | "description": "Nested forms base class for Yii2 Framework.", 4 | "type": "yii2-extension", 5 | "keywords": ["yii2", "yii 2"], 6 | "license": "BSD-3-Clause", 7 | "authors": [ 8 | { 9 | "name": "Dmitry Eliseev", 10 | "email": "mail@elisdn.ru", 11 | "homepage": "http://www.elisdn.ru" 12 | } 13 | ], 14 | "support": { 15 | "issues": "https://github.com/ElisDN/yii2-composite-form/issues?state=open", 16 | "source": "https://github.com/ElisDN/yii2-composite-form" 17 | }, 18 | "require": { 19 | "yiisoft/yii2": "~2.0" 20 | }, 21 | "require-dev": { 22 | "phpunit/phpunit": "4.*" 23 | }, 24 | "autoload": { 25 | "psr-4": { 26 | "elisdn\\compositeForm\\": "src/" 27 | } 28 | }, 29 | "autoload-dev": { 30 | "psr-4": { 31 | "elisdn\\compositeForm\\tests\\": "tests/" 32 | } 33 | }, 34 | "repositories": [ 35 | { 36 | "type": "composer", 37 | "url": "https://asset-packagist.org" 38 | } 39 | ] 40 | } -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | ./tests 11 | 12 | 13 | 14 | 15 | ./src/ 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/CompositeForm.php: -------------------------------------------------------------------------------- 1 | 6 | * @license https://github.com/ElisDN/yii2-composite-form/blob/master/LICENSE.md 7 | * @link https://github.com/ElisDN/yii2-composite-form 8 | */ 9 | 10 | namespace elisdn\compositeForm; 11 | 12 | use yii\base\Model; 13 | use yii\helpers\ArrayHelper; 14 | 15 | abstract class CompositeForm extends Model 16 | { 17 | /** 18 | * @var Model[]|array[] 19 | */ 20 | protected $_forms = []; 21 | 22 | /** 23 | * @return array of internal forms like ['meta', 'values'] 24 | */ 25 | abstract protected function internalForms(); 26 | 27 | public function load($data, $formName = null) 28 | { 29 | $success = parent::load($data, $formName); 30 | foreach ($this->_forms as $name => $form) { 31 | if (is_array($form)) { 32 | $success = Model::loadMultiple($form, $data, $formName === null ? null : $name) || $success; 33 | } else { 34 | $success = $form->load($data, $formName !== '' ? null : $name) || $success; 35 | } 36 | } 37 | return $success; 38 | } 39 | 40 | public function validate($attributeNames = null, $clearErrors = true) 41 | { 42 | if ($attributeNames !== null) { 43 | $parentNames = array_filter($attributeNames, 'is_string'); 44 | $success = $parentNames ? parent::validate($parentNames, $clearErrors) : true; 45 | } else { 46 | $success = parent::validate(null, $clearErrors); 47 | } 48 | foreach ($this->_forms as $name => $form) { 49 | if ($attributeNames === null || array_key_exists($name, $attributeNames) || in_array($name, $attributeNames, true)) { 50 | $innerNames = ArrayHelper::getValue($attributeNames, $name); 51 | if (is_array($form)) { 52 | $success = Model::validateMultiple($form, $innerNames) && $success; 53 | } else { 54 | $success = $form->validate($innerNames, $clearErrors) && $success; 55 | } 56 | } 57 | } 58 | return $success; 59 | } 60 | 61 | public function hasErrors($attribute = null) 62 | { 63 | if ($attribute !== null && mb_strpos($attribute, '.') === false) { 64 | return parent::hasErrors($attribute); 65 | } 66 | if (parent::hasErrors($attribute)) { 67 | return true; 68 | } 69 | foreach ($this->_forms as $name => $form) { 70 | if (is_array($form)) { 71 | foreach ($form as $i => $item) { 72 | if ($attribute === null) { 73 | if ($item->hasErrors()) { 74 | return true; 75 | } 76 | } elseif (mb_strpos($attribute, $name . '.' . $i . '.') === 0) { 77 | if ($item->hasErrors(mb_substr($attribute, mb_strlen($name . '.' . $i . '.')))) { 78 | return true; 79 | } 80 | } 81 | } 82 | } else { 83 | if ($attribute === null) { 84 | if ($form->hasErrors()) { 85 | return true; 86 | } 87 | } elseif (mb_strpos($attribute, $name . '.') === 0) { 88 | if ($form->hasErrors(mb_substr($attribute, mb_strlen($name . '.')))) { 89 | return true; 90 | } 91 | } 92 | } 93 | } 94 | return false; 95 | } 96 | 97 | public function getErrors($attribute = null) 98 | { 99 | $result = parent::getErrors($attribute); 100 | foreach ($this->_forms as $name => $form) { 101 | if (is_array($form)) { 102 | /** @var Model[] $form */ 103 | foreach ($form as $i => $item) { 104 | foreach ($item->getErrors() as $attr => $errors) { 105 | /** @var array $errors */ 106 | $errorAttr = $name . '.' . $i . '.' . $attr; 107 | if ($attribute === null) { 108 | foreach ($errors as $error) { 109 | $result[$errorAttr][] = $error; 110 | } 111 | } elseif ($errorAttr === $attribute) { 112 | foreach ($errors as $error) { 113 | $result[] = $error; 114 | } 115 | } 116 | } 117 | } 118 | } else { 119 | foreach ($form->getErrors() as $attr => $errors) { 120 | /** @var array $errors */ 121 | $errorAttr = $name . '.' . $attr; 122 | if ($attribute === null) { 123 | foreach ($errors as $error) { 124 | $result[$errorAttr][] = $error; 125 | } 126 | } elseif ($errorAttr === $attribute) { 127 | foreach ($errors as $error) { 128 | $result[] = $error; 129 | } 130 | } 131 | } 132 | } 133 | } 134 | return $result; 135 | } 136 | 137 | public function getFirstErrors() 138 | { 139 | $result = parent::getFirstErrors(); 140 | foreach ($this->_forms as $name => $form) { 141 | if (is_array($form)) { 142 | foreach ($form as $i => $item) { 143 | foreach ($item->getFirstErrors() as $attr => $error) { 144 | $result[$name . '.' . $i . '.' . $attr] = $error; 145 | } 146 | } 147 | } else { 148 | foreach ($form->getFirstErrors() as $attr => $error) { 149 | $result[$name . '.' . $attr] = $error; 150 | } 151 | } 152 | } 153 | return $result; 154 | } 155 | 156 | public function __get($name) 157 | { 158 | if (isset($this->_forms[$name])) { 159 | return $this->_forms[$name]; 160 | } 161 | return parent::__get($name); 162 | } 163 | 164 | public function __set($name, $value) 165 | { 166 | if (in_array($name, $this->internalForms(), true)) { 167 | $this->_forms[$name] = $value; 168 | } else { 169 | parent::__set($name, $value); 170 | } 171 | } 172 | 173 | public function __isset($name) 174 | { 175 | return isset($this->_forms[$name]) || parent::__isset($name); 176 | } 177 | } -------------------------------------------------------------------------------- /tests/LoadActiveFormTest.php: -------------------------------------------------------------------------------- 1 | 6 | * @license https://github.com/ElisDN/yii2-composite-form/blob/master/LICENSE.md 7 | * @link https://github.com/ElisDN/yii2-composite-form 8 | */ 9 | 10 | namespace elisdn\compositeForm\tests; 11 | 12 | use elisdn\compositeForm\tests\_forms\OnlyNestedProductForm; 13 | use elisdn\compositeForm\tests\_forms\ProductForm; 14 | 15 | class LoadActiveFormTest extends TestCase 16 | { 17 | public function testWholeForm() 18 | { 19 | $data = [ 20 | 'ProductForm' => [ 21 | 'code' => 'P100', 22 | 'name' => 'Product Name', 23 | ], 24 | 'MetaForm' => [ 25 | 'title' => 'Meta Title', 26 | 'description' => 'Meta Description', 27 | ], 28 | 'ValueForm' => [ 29 | ['value' => '101'], 30 | ['value' => '102'], 31 | ['value' => '103'], 32 | ], 33 | ]; 34 | 35 | $form = new ProductForm(3); 36 | 37 | $this->assertTrue($form->load($data)); 38 | 39 | $this->assertEquals($data['ProductForm']['code'], $form->code); 40 | $this->assertEquals($data['ProductForm']['name'], $form->name); 41 | 42 | $this->assertEquals($data['MetaForm']['title'], $form->meta->title); 43 | $this->assertEquals($data['MetaForm']['description'], $form->meta->description); 44 | 45 | $this->assertCount(3, $values = $form->values); 46 | 47 | $this->assertEquals($data['ValueForm'][0]['value'], $values[0]->value); 48 | $this->assertEquals($data['ValueForm'][1]['value'], $values[1]->value); 49 | $this->assertEquals($data['ValueForm'][2]['value'], $values[2]->value); 50 | } 51 | 52 | public function testPartialForm() 53 | { 54 | $data = [ 55 | 'ProductForm' => [ 56 | 'code' => 'P100', 57 | 'name' => 'Product Name', 58 | ], 59 | 'MetaForm' => [ 60 | 'title' => 'Meta Title', 61 | 'description' => 'Meta Description', 62 | ], 63 | ]; 64 | 65 | $form = new ProductForm(3); 66 | 67 | $this->assertTrue($form->load($data)); 68 | 69 | $this->assertEquals($data['ProductForm']['code'], $form->code); 70 | $this->assertEquals($data['ProductForm']['name'], $form->name); 71 | 72 | $this->assertEquals($data['MetaForm']['title'], $form->meta->title); 73 | $this->assertEquals($data['MetaForm']['description'], $form->meta->description); 74 | 75 | $this->assertCount(3, $values = $form->values); 76 | 77 | $this->assertNull($values[0]->value); 78 | $this->assertNull($values[1]->value); 79 | $this->assertNull($values[2]->value); 80 | } 81 | 82 | public function testOnlyInternalForms() 83 | { 84 | $data = [ 85 | 'MetaForm' => [ 86 | 'title' => 'Meta Title', 87 | 'description' => 'Meta Description', 88 | ], 89 | ]; 90 | 91 | $form = new OnlyNestedProductForm(); 92 | 93 | $this->assertTrue($form->load($data)); 94 | 95 | $this->assertEquals($data['MetaForm']['title'], $form->meta->title); 96 | $this->assertEquals($data['MetaForm']['description'], $form->meta->description); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /tests/LoadApiTest.php: -------------------------------------------------------------------------------- 1 | 6 | * @license https://github.com/ElisDN/yii2-composite-form/blob/master/LICENSE.md 7 | * @link https://github.com/ElisDN/yii2-composite-form 8 | */ 9 | 10 | namespace elisdn\compositeForm\tests; 11 | 12 | use elisdn\compositeForm\tests\_forms\OnlyNestedProductForm; 13 | use elisdn\compositeForm\tests\_forms\ProductForm; 14 | 15 | class LoadApiTest extends TestCase 16 | { 17 | public function testWholeForm() 18 | { 19 | $data = [ 20 | 'code' => 'P100', 21 | 'name' => 'Product Name', 22 | 'meta' => [ 23 | 'title' => 'Meta Title', 24 | 'description' => 'Meta Description', 25 | ], 26 | 'values' => [ 27 | ['value' => '101'], 28 | ['value' => '102'], 29 | ['value' => '103'], 30 | ], 31 | ]; 32 | 33 | $form = new ProductForm(3); 34 | 35 | $this->assertTrue($form->load($data, '')); 36 | 37 | $this->assertEquals($data['code'], $form->code); 38 | $this->assertEquals($data['name'], $form->name); 39 | 40 | $this->assertEquals($data['meta']['title'], $form->meta->title); 41 | $this->assertEquals($data['meta']['description'], $form->meta->description); 42 | 43 | $this->assertCount(3, $values = $form->values); 44 | 45 | $this->assertEquals($data['values'][0]['value'], $values[0]->value); 46 | $this->assertEquals($data['values'][1]['value'], $values[1]->value); 47 | $this->assertEquals($data['values'][2]['value'], $values[2]->value); 48 | } 49 | 50 | public function testOnlyInternalForms() 51 | { 52 | $data = [ 53 | 'meta' => [ 54 | 'title' => 'Meta Title', 55 | 'description' => 'Meta Description', 56 | ], 57 | ]; 58 | 59 | $form = new OnlyNestedProductForm(); 60 | 61 | $this->assertTrue($form->load($data, '')); 62 | 63 | $this->assertEquals($data['meta']['title'], $form->meta->title); 64 | $this->assertEquals($data['meta']['description'], $form->meta->description); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | 6 | * @license https://github.com/ElisDN/yii2-composite-form/blob/master/LICENSE.md 7 | * @link https://github.com/ElisDN/yii2-composite-form 8 | */ 9 | 10 | namespace elisdn\compositeForm\tests; 11 | 12 | use yii\console\Application; 13 | 14 | abstract class TestCase extends \PHPUnit_Framework_TestCase 15 | { 16 | protected function setUp() 17 | { 18 | parent::setUp(); 19 | $this->mockApplication(); 20 | } 21 | 22 | protected function tearDown() 23 | { 24 | $this->destroyApplication(); 25 | parent::tearDown(); 26 | } 27 | 28 | protected function mockApplication() 29 | { 30 | new Application([ 31 | 'id' => 'testapp', 32 | 'basePath' => __DIR__, 33 | 'vendorPath' => dirname(__DIR__) . '/vendor', 34 | 'runtimePath' => __DIR__ . '/runtime', 35 | ]); 36 | } 37 | 38 | protected function destroyApplication() 39 | { 40 | \Yii::$app = null; 41 | } 42 | } -------------------------------------------------------------------------------- /tests/ValidateTest.php: -------------------------------------------------------------------------------- 1 | 6 | * @license https://github.com/ElisDN/yii2-composite-form/blob/master/LICENSE.md 7 | * @link https://github.com/ElisDN/yii2-composite-form 8 | */ 9 | 10 | namespace elisdn\compositeForm\tests; 11 | 12 | use elisdn\compositeForm\tests\_forms\OnlyNestedProductForm; 13 | use elisdn\compositeForm\tests\_forms\ProductForm; 14 | 15 | class ValidateTest extends TestCase 16 | { 17 | public function testValidWholeForm() 18 | { 19 | $data = [ 20 | 'code' => 'P100', 21 | 'name' => 'Product Name', 22 | 'meta' => [ 23 | 'title' => 'Meta Title', 24 | 'description' => 'Meta Description', 25 | ], 26 | 'values' => [ 27 | ['value' => '101'], 28 | ['value' => '102'], 29 | ['value' => '103'], 30 | ], 31 | ]; 32 | 33 | $form = new ProductForm(3); 34 | 35 | $form->load($data, ''); 36 | 37 | $this->assertTrue($form->validate()); 38 | $this->assertFalse($form->hasErrors()); 39 | $this->assertEmpty($form->getErrors()); 40 | } 41 | 42 | public function testValidWithoutValues() 43 | { 44 | $data = [ 45 | 'code' => 'P100', 46 | 'name' => 'Product Name', 47 | 'meta' => [ 48 | 'title' => 'Meta Title', 49 | 'description' => 'Meta Description', 50 | ], 51 | 'values' => [], 52 | ]; 53 | 54 | $form = new ProductForm(0); 55 | 56 | $form->load($data, ''); 57 | 58 | $this->assertTrue($form->validate()); 59 | $this->assertFalse($form->hasErrors()); 60 | $this->assertEmpty($form->getErrors()); 61 | } 62 | 63 | public function testNotValidWholeForm() 64 | { 65 | $data = [ 66 | 'code' => null, 67 | 'name' => 'Product Name', 68 | 'meta' => [ 69 | 'title' => null, 70 | 'description' => 'Meta Description', 71 | ], 72 | 'values' => [ 73 | ['value' => '101'], 74 | ['value' => ''], 75 | ['value' => '103'], 76 | ], 77 | ]; 78 | 79 | $form = new ProductForm(3); 80 | 81 | $form->load($data, ''); 82 | 83 | $this->assertFalse($form->validate()); 84 | $this->assertTrue($form->hasErrors()); 85 | 86 | $this->assertEquals([ 87 | 'code' => ['Code cannot be blank.'], 88 | 'meta.title' => ['Title cannot be blank.'], 89 | 'values.1.value' => ['Value cannot be blank.'], 90 | ], $form->getErrors()); 91 | 92 | $this->assertEquals(['Code cannot be blank.'], $form->getErrors('code')); 93 | $this->assertEquals(['Title cannot be blank.'], $form->getErrors('meta.title')); 94 | $this->assertEquals(['Value cannot be blank.'], $form->getErrors('values.1.value')); 95 | 96 | $this->assertEquals([], $form->getErrors('name')); 97 | $this->assertEquals([], $form->getErrors('meta.description')); 98 | $this->assertEquals([], $form->getErrors('values.2.value')); 99 | 100 | $this->assertTrue($form->hasErrors('code')); 101 | $this->assertFalse($form->hasErrors('name')); 102 | $this->assertTrue($form->hasErrors('meta.title')); 103 | $this->assertFalse($form->hasErrors('meta.description')); 104 | $this->assertTrue($form->hasErrors('values.1.value')); 105 | $this->assertFalse($form->hasErrors('values.2.value')); 106 | 107 | $this->assertEquals([ 108 | 'code' => 'Code cannot be blank.', 109 | 'meta.title' => 'Title cannot be blank.', 110 | 'values.1.value' => 'Value cannot be blank.', 111 | ], $form->getFirstErrors()); 112 | } 113 | 114 | public function testNotValidInternalForms() 115 | { 116 | $data = [ 117 | 'code' => 'P100', 118 | 'name' => 'Product Name', 119 | 'meta' => [ 120 | 'title' => null, 121 | 'description' => 'Meta Description', 122 | ], 123 | 'values' => [ 124 | ['value' => '101'], 125 | ['value' => ''], 126 | ['value' => '103'], 127 | ], 128 | ]; 129 | 130 | $form = new ProductForm(3); 131 | 132 | $form->load($data, ''); 133 | 134 | $this->assertFalse($form->validate()); 135 | $this->assertTrue($form->hasErrors()); 136 | 137 | $this->assertEquals([ 138 | 'meta.title' => ['Title cannot be blank.'], 139 | 'values.1.value' => ['Value cannot be blank.'], 140 | ], $form->getErrors()); 141 | 142 | $this->assertFalse($form->hasErrors('code')); 143 | $this->assertTrue($form->hasErrors('meta.title')); 144 | $this->assertTrue($form->hasErrors('values.1.value')); 145 | 146 | $this->assertEquals([ 147 | 'meta.title' => 'Title cannot be blank.', 148 | 'values.1.value' => 'Value cannot be blank.', 149 | ], $form->getFirstErrors()); 150 | } 151 | 152 | public function testValidAttributeNames() 153 | { 154 | $data = [ 155 | 'code' => 'P100', 156 | 'name' => 'Product Name', 157 | 'meta' => [ 158 | 'title' => 'Meta Title', 159 | 'description' => 'Meta Description', 160 | ], 161 | 'values' => [ 162 | ['value' => '101'], 163 | ['value' => '103'], 164 | ], 165 | ]; 166 | 167 | $form = new ProductForm(0); 168 | 169 | $form->load($data, ''); 170 | 171 | $this->assertTrue($form->validate(['code'])); 172 | $this->assertTrue($form->validate(['name'])); 173 | $this->assertTrue($form->validate(['meta'])); 174 | $this->assertTrue($form->validate(['meta' => ['title']])); 175 | $this->assertTrue($form->validate(['meta' => ['description']])); 176 | $this->assertTrue($form->validate(['meta' => ['title', 'description']])); 177 | $this->assertTrue($form->validate(['values'])); 178 | $this->assertTrue($form->validate(['values' => ['value']])); 179 | } 180 | 181 | public function testNotValidAttributeNames() 182 | { 183 | $data = [ 184 | 'code' => null, 185 | 'name' => 'Product Name', 186 | 'meta' => [ 187 | 'title' => null, 188 | 'description' => 'Meta Description', 189 | ], 190 | 'values' => [ 191 | ['value' => '101'], 192 | ['value' => ''], 193 | ], 194 | ]; 195 | 196 | $form = new ProductForm(2); 197 | 198 | $form->load($data, ''); 199 | 200 | $this->assertFalse($form->validate(['code'])); 201 | $this->assertTrue($form->validate(['name'])); 202 | $this->assertFalse($form->validate(['meta'])); 203 | $this->assertFalse($form->validate(['meta' => ['title']])); 204 | $this->assertTrue($form->validate(['meta' => ['description']])); 205 | $this->assertFalse($form->validate(['meta' => ['title', 'description']])); 206 | $this->assertFalse($form->validate(['values'])); 207 | $this->assertFalse($form->validate(['values' => ['value']])); 208 | } 209 | 210 | public function testValidOnlyNestedForms() 211 | { 212 | $data = [ 213 | 'meta' => [ 214 | 'title' => 'Meta Title', 215 | 'description' => 'Meta Description', 216 | ], 217 | ]; 218 | 219 | $form = new OnlyNestedProductForm(); 220 | 221 | $form->load($data, ''); 222 | 223 | $this->assertTrue($form->validate()); 224 | $this->assertFalse($form->hasErrors()); 225 | $this->assertEmpty($form->getErrors()); 226 | } 227 | 228 | public function testNotValidOnlyNestedForms() 229 | { 230 | $data = [ 231 | 'meta' => [ 232 | 'title' => null, 233 | 'description' => 'Meta Description', 234 | ], 235 | ]; 236 | 237 | $form = new OnlyNestedProductForm(); 238 | 239 | $form->load($data, ''); 240 | 241 | $this->assertFalse($form->validate()); 242 | $this->assertTrue($form->hasErrors()); 243 | 244 | $this->assertEquals([ 245 | 'meta.title' => ['Title cannot be blank.'], 246 | ], $form->getErrors()); 247 | 248 | $this->assertEquals([ 249 | 'meta.title' => 'Title cannot be blank.', 250 | ], $form->getFirstErrors()); 251 | } 252 | } -------------------------------------------------------------------------------- /tests/_forms/MetaForm.php: -------------------------------------------------------------------------------- 1 | 6 | * @license https://github.com/ElisDN/yii2-composite-form/blob/master/LICENSE.md 7 | * @link https://github.com/ElisDN/yii2-composite-form 8 | */ 9 | 10 | namespace elisdn\compositeForm\tests\_forms; 11 | 12 | use yii\base\Model; 13 | 14 | class MetaForm extends Model 15 | { 16 | public $title; 17 | public $description; 18 | 19 | public function rules() 20 | { 21 | return [ 22 | [['title'], 'required'], 23 | [['title', 'description'], 'string'], 24 | ]; 25 | } 26 | } -------------------------------------------------------------------------------- /tests/_forms/OnlyNestedProductForm.php: -------------------------------------------------------------------------------- 1 | 6 | * @license https://github.com/ElisDN/yii2-composite-form/blob/master/LICENSE.md 7 | * @link https://github.com/ElisDN/yii2-composite-form 8 | */ 9 | 10 | namespace elisdn\compositeForm\tests\_forms; 11 | 12 | use elisdn\compositeForm\CompositeForm; 13 | 14 | /** 15 | * @property MetaForm $meta 16 | */ 17 | class OnlyNestedProductForm extends CompositeForm 18 | { 19 | public function __construct($config = []) 20 | { 21 | $this->meta = new MetaForm(); 22 | parent::__construct($config); 23 | } 24 | 25 | protected function internalForms() 26 | { 27 | return ['meta']; 28 | } 29 | } -------------------------------------------------------------------------------- /tests/_forms/ProductForm.php: -------------------------------------------------------------------------------- 1 | 6 | * @license https://github.com/ElisDN/yii2-composite-form/blob/master/LICENSE.md 7 | * @link https://github.com/ElisDN/yii2-composite-form 8 | */ 9 | 10 | namespace elisdn\compositeForm\tests\_forms; 11 | 12 | use elisdn\compositeForm\CompositeForm; 13 | 14 | /** 15 | * @property MetaForm $meta 16 | * @property ValueForm[] $values 17 | */ 18 | class ProductForm extends CompositeForm 19 | { 20 | public $code; 21 | public $name; 22 | 23 | /** 24 | * @param integer $valuesCount 25 | * @param array $config 26 | */ 27 | public function __construct($valuesCount, $config = []) 28 | { 29 | $this->meta = new MetaForm(); 30 | $this->values = $valuesCount ? array_map(function () { 31 | return new ValueForm(); 32 | }, range(1, $valuesCount)) : []; 33 | parent::__construct($config); 34 | } 35 | 36 | public function rules() 37 | { 38 | return [ 39 | [['code'], 'required'], 40 | [['code', 'name'], 'string'], 41 | ]; 42 | } 43 | 44 | protected function internalForms() 45 | { 46 | return ['meta', 'values']; 47 | } 48 | } -------------------------------------------------------------------------------- /tests/_forms/ValueForm.php: -------------------------------------------------------------------------------- 1 | 6 | * @license https://github.com/ElisDN/yii2-composite-form/blob/master/LICENSE.md 7 | * @link https://github.com/ElisDN/yii2-composite-form 8 | */ 9 | 10 | namespace elisdn\compositeForm\tests\_forms; 11 | 12 | use yii\base\Model; 13 | 14 | class ValueForm extends Model 15 | { 16 | public $value; 17 | 18 | public function rules() 19 | { 20 | return [ 21 | ['value', 'required'], 22 | ]; 23 | } 24 | } -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | 6 | * @license https://github.com/ElisDN/yii2-composite-form/blob/master/LICENSE.md 7 | * @link https://github.com/ElisDN/yii2-composite-form 8 | */ 9 | 10 | defined('YII_DEBUG') or define('YII_DEBUG', true); 11 | defined('YII_ENV') or define('YII_ENV', 'test'); 12 | 13 | require(__DIR__ . '/../vendor/autoload.php'); 14 | require(__DIR__ . '/../vendor/yiisoft/yii2/Yii.php'); --------------------------------------------------------------------------------