├── tests
├── phpunit.xml
├── bootstrap.php
├── fixtures
│ ├── models
│ │ ├── Tag.php
│ │ ├── Category.php
│ │ ├── ProductDescription.php
│ │ ├── Certificate.php
│ │ ├── CertificateForm.php
│ │ ├── Attachment.php
│ │ ├── ProductForm.php
│ │ ├── Product.php
│ │ └── AttachmentForm.php
│ └── data
│ │ └── sqlite.sql
└── unit
│ ├── WFileIteratorTest.php
│ ├── WActiveRecordTest.php
│ ├── WTempFileTest.php
│ ├── WFormRelationTest.php
│ ├── WFormBehaviorTest.php
│ ├── WFormTest.php
│ ├── WFormRelationBelongsToTest.php
│ ├── WFormRelationHasOneTest.php
│ ├── WFormRelationManyManyTest.php
│ └── WFormRelationHasManyTest.php
├── WActiveRecord.php
├── examples
└── product
│ ├── models
│ ├── Tag.php
│ ├── Category.php
│ ├── ProductDescription.php
│ ├── Certificate.php
│ ├── forms
│ │ ├── CertificateForm.php
│ │ ├── ProductForm.php
│ │ └── AttachmentForm.php
│ ├── Attachment.php
│ └── Product.php
│ ├── views
│ └── product
│ │ ├── index.php
│ │ └── edit.php
│ ├── controllers
│ └── ProductController.php
│ ├── product.sql
│ └── README.md
├── WTempFile.php
├── WFormRelationBelongsTo.php
├── WFileIterator.php
├── WFormRelationHasOne.php
├── js
└── jquery.multiplyforms.js
├── WFormRelationHasMany.php
├── WFormRelationManyMany.php
├── WFormRelation.php
├── WFormBehavior.php
├── README.md
└── WForm.php
/tests/phpunit.xml:
--------------------------------------------------------------------------------
1 |
9 |
--------------------------------------------------------------------------------
/WActiveRecord.php:
--------------------------------------------------------------------------------
1 |
4 | * @link http://weavora.com
5 | * @copyright Copyright (c) 2011 Weavora LLC
6 | */
7 |
8 | class WActiveRecord extends CActiveRecord {
9 |
10 | // @todo any ideas how to prevent using custom event for that ?
11 | public function onUnsafeAttribute($name, $value)
12 | {
13 | $event = new CEvent($this, array('name' => $name, 'value' => $value));
14 | $this->raiseEvent('onUnsafeAttribute', $event);
15 | return parent::onUnsafeAttribute($name, $value);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/tests/bootstrap.php:
--------------------------------------------------------------------------------
1 | dirname(__FILE__) . '/fixtures/'
19 | ));
20 |
--------------------------------------------------------------------------------
/tests/fixtures/models/Tag.php:
--------------------------------------------------------------------------------
1 |
4 | * @link http://weavora.com
5 | * @copyright Copyright (c) 2011 Weavora LLC
6 | */
7 |
8 | class Tag extends WActiveRecord
9 | {
10 |
11 | public static function model($className=__CLASS__)
12 | {
13 | return parent::model($className);
14 | }
15 |
16 | public function tableName()
17 | {
18 | return 'tags';
19 | }
20 |
21 | public function rules()
22 | {
23 | return array(
24 | array('name', 'required'),
25 | );
26 | }
27 |
28 | public function attributeLabels()
29 | {
30 | return array(
31 | 'id' => 'ID',
32 | 'name' => 'Name',
33 | );
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/examples/product/models/Tag.php:
--------------------------------------------------------------------------------
1 |
4 | * @link http://weavora.com
5 | * @copyright Copyright (c) 2011 Weavora LLC
6 | */
7 |
8 | class Tag extends WActiveRecord
9 | {
10 |
11 | public static function model($className=__CLASS__)
12 | {
13 | return parent::model($className);
14 | }
15 |
16 | public function tableName()
17 | {
18 | return 'tags';
19 | }
20 |
21 | public function rules()
22 | {
23 | return array(
24 | array('name', 'required'),
25 | array('name', 'unique'),
26 | );
27 | }
28 |
29 | public function attributeLabels()
30 | {
31 | return array(
32 | 'id' => 'ID',
33 | 'name' => 'Name',
34 | );
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/examples/product/models/Category.php:
--------------------------------------------------------------------------------
1 |
4 | * @link http://weavora.com
5 | * @copyright Copyright (c) 2011 Weavora LLC
6 | */
7 |
8 | class Category extends WActiveRecord
9 | {
10 |
11 | public static function model($className=__CLASS__)
12 | {
13 | return parent::model($className);
14 | }
15 |
16 | public function tableName()
17 | {
18 | return 'categories';
19 | }
20 |
21 | public function rules()
22 | {
23 | return array(
24 | array('name', 'required'),
25 | array('name', 'unique'),
26 | );
27 | }
28 |
29 | public function attributeLabels()
30 | {
31 | return array(
32 | 'id' => 'ID',
33 | 'name' => 'Name',
34 | );
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/tests/fixtures/models/Category.php:
--------------------------------------------------------------------------------
1 |
4 | * @link http://weavora.com
5 | * @copyright Copyright (c) 2011 Weavora LLC
6 | */
7 |
8 | class Category extends WActiveRecord
9 | {
10 |
11 | public static function model($className=__CLASS__)
12 | {
13 | return parent::model($className);
14 | }
15 |
16 | public function tableName()
17 | {
18 | return 'categories';
19 | }
20 |
21 | public function rules()
22 | {
23 | return array(
24 | array('name', 'required'),
25 | array('name', 'unique'),
26 | );
27 | }
28 |
29 | public function attributeLabels()
30 | {
31 | return array(
32 | 'id' => 'ID',
33 | 'name' => 'Name',
34 | );
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/examples/product/models/ProductDescription.php:
--------------------------------------------------------------------------------
1 |
4 | * @link http://weavora.com
5 | * @copyright Copyright (c) 2011 Weavora LLC
6 | */
7 |
8 | class ProductDescription extends WActiveRecord
9 | {
10 |
11 | public static function model($className=__CLASS__)
12 | {
13 | return parent::model($className);
14 | }
15 |
16 | public function tableName()
17 | {
18 | return 'product_descriptions';
19 | }
20 |
21 | public function rules()
22 | {
23 | return array(
24 | array('color, size, product_id', 'safe'),
25 | );
26 | }
27 |
28 | public function attributeLabels()
29 | {
30 | return array(
31 | 'id' => 'ID',
32 | 'color' => 'Color',
33 | 'size' => 'Size',
34 | );
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/tests/fixtures/models/ProductDescription.php:
--------------------------------------------------------------------------------
1 |
4 | * @link http://weavora.com
5 | * @copyright Copyright (c) 2011 Weavora LLC
6 | */
7 |
8 | class ProductDescription extends WActiveRecord
9 | {
10 |
11 | public static function model($className=__CLASS__)
12 | {
13 | return parent::model($className);
14 | }
15 |
16 | public function tableName()
17 | {
18 | return 'product_descriptions';
19 | }
20 |
21 | public function rules()
22 | {
23 | return array(
24 | array('size', 'required'),
25 | array('color, product_id', 'safe'),
26 | );
27 | }
28 |
29 | public function attributeLabels()
30 | {
31 | return array(
32 | 'id' => 'ID',
33 | 'color' => 'Color',
34 | 'size' => 'Size',
35 | );
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/examples/product/models/Certificate.php:
--------------------------------------------------------------------------------
1 |
4 | * @link http://weavora.com
5 | * @copyright Copyright (c) 2011 Weavora LLC
6 | */
7 |
8 | class Certificate extends WActiveRecord
9 | {
10 |
11 | public static function model($className=__CLASS__)
12 | {
13 | return parent::model($className);
14 | }
15 |
16 | public function tableName()
17 | {
18 | return 'certificates';
19 | }
20 |
21 | public function rules()
22 | {
23 | return array(
24 | array('name', 'required'),
25 | );
26 | }
27 |
28 | public function relations()
29 | {
30 | return array(
31 | 'image' => array(self::HAS_ONE, 'Attachment', 'object_id', 'condition' => 'image.object_type=:object_type', 'params' => array('object_type' => Attachment::OBJECT_TYPE_CERTIFICATE)),
32 | );
33 | }
34 |
35 | public function attributeLabels()
36 | {
37 | return array(
38 | 'id' => 'ID',
39 | 'name' => 'Name',
40 | );
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/tests/fixtures/models/Certificate.php:
--------------------------------------------------------------------------------
1 |
4 | * @link http://weavora.com
5 | * @copyright Copyright (c) 2011 Weavora LLC
6 | */
7 |
8 | class Certificate extends WActiveRecord
9 | {
10 |
11 | public static function model($className=__CLASS__)
12 | {
13 | return parent::model($className);
14 | }
15 |
16 | public function tableName()
17 | {
18 | return 'certificates';
19 | }
20 |
21 | public function rules()
22 | {
23 | return array(
24 | array('name', 'required'),
25 | );
26 | }
27 |
28 | public function relations()
29 | {
30 | return array(
31 | 'image' => array(self::HAS_ONE, 'Attachment', 'object_id', 'condition' => 'image.object_type=:object_type', 'params' => array('object_type' => Attachment::OBJECT_TYPE_CERTIFICATE)),
32 | );
33 | }
34 |
35 | public function attributeLabels()
36 | {
37 | return array(
38 | 'id' => 'ID',
39 | 'name' => 'Name',
40 | );
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/tests/fixtures/models/CertificateForm.php:
--------------------------------------------------------------------------------
1 |
4 | * @link http://weavora.com
5 | * @copyright Copyright (c) 2011 Weavora LLC
6 | */
7 |
8 | class CertificateForm extends Certificate
9 | {
10 |
11 | public static function model($className=__CLASS__)
12 | {
13 | return parent::model($className);
14 | }
15 |
16 | public function relations()
17 | {
18 | return array_merge(parent::relations(), array(
19 | 'image' => array(self::HAS_ONE, 'AttachmentForm', 'object_id', 'condition' => 'image.object_type=:object_type', 'params' => array('object_type' => Attachment::OBJECT_TYPE_CERTIFICATE)),
20 | ));
21 | }
22 |
23 | public function behaviors()
24 | {
25 | return array_merge(
26 | parent::behaviors(),
27 | array(
28 | 'wform' => array(
29 | 'class' => 'ext.wform.WFormBehavior',
30 | 'relations' => array(
31 | 'image' => array(),
32 | ),
33 | ),
34 | )
35 | );
36 | }
37 |
38 |
39 | }
40 |
--------------------------------------------------------------------------------
/examples/product/models/forms/CertificateForm.php:
--------------------------------------------------------------------------------
1 |
4 | * @link http://weavora.com
5 | * @copyright Copyright (c) 2011 Weavora LLC
6 | */
7 |
8 | class CertificateForm extends Certificate
9 | {
10 |
11 | public static function model($className=__CLASS__)
12 | {
13 | return parent::model($className);
14 | }
15 |
16 | public function relations()
17 | {
18 | return array_merge(parent::relations(), array(
19 | 'image' => array(self::HAS_ONE, 'AttachmentForm', 'object_id', 'condition' => 'image.object_type=:object_type', 'params' => array('object_type' => Attachment::OBJECT_TYPE_CERTIFICATE)),
20 | ));
21 | }
22 |
23 | public function behaviors()
24 | {
25 | return array_merge(
26 | parent::behaviors(),
27 | array(
28 | 'wform' => array(
29 | 'class' => 'ext.wform.WFormBehavior',
30 | 'relations' => array(
31 | 'image' => array(),
32 | ),
33 | ),
34 | )
35 | );
36 | }
37 |
38 |
39 | }
40 |
--------------------------------------------------------------------------------
/WTempFile.php:
--------------------------------------------------------------------------------
1 | _tempDirectory = $tempDirectory;
12 | }
13 |
14 | public function setFile($file)
15 | {
16 | $this->_file = $file;
17 | }
18 |
19 | public function isValid()
20 | {
21 | return is_file($this->getPath()) && file_exists($this->getPath());
22 | }
23 |
24 | public function getPath()
25 | {
26 | return $this->_tempDirectory . '/' . $this->getFile();
27 | }
28 |
29 | public function saveAs($destFile)
30 | {
31 | if (!$this->isValid())
32 | return false;
33 |
34 | return copy($this->getPath(), $destFile);
35 | }
36 |
37 | public function upload($sourceFile)
38 | {
39 | return move_uploaded_file($sourceFile, $this->getPath());
40 | }
41 |
42 | public function getFile()
43 | {
44 | if (empty($this->_file))
45 | $this->_file = tempnam($this->_tempDirectory, "php");
46 |
47 | return basename($this->_file);
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/tests/fixtures/models/Attachment.php:
--------------------------------------------------------------------------------
1 |
4 | * @link http://weavora.com
5 | * @copyright Copyright (c) 2011 Weavora LLC
6 | */
7 |
8 | class Attachment extends WActiveRecord
9 | {
10 |
11 | const OBJECT_TYPE_PRODUCT_IMAGE = 'product_image';
12 | const OBJECT_TYPE_CERTIFICATE = 'certificate';
13 |
14 | public static function model($className=__CLASS__)
15 | {
16 | return parent::model($className);
17 | }
18 |
19 | public function tableName()
20 | {
21 | return 'attachments';
22 | }
23 |
24 | public function rules()
25 | {
26 | return array(
27 | array('object_id', 'numerical', 'integerOnly'=>true),
28 | array('object_type', 'length', 'max'=>13),
29 | array('file', 'length', 'max'=>250),
30 | array('file', 'required'),
31 | );
32 | }
33 |
34 | public function attributeLabels()
35 | {
36 | return array(
37 | 'id' => 'ID',
38 | 'object_id' => 'Object',
39 | 'object_type' => 'Object Type',
40 | 'file' => 'File',
41 | 'file_origin' => 'File Origin',
42 | );
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/examples/product/views/product/index.php:
--------------------------------------------------------------------------------
1 |
Products
2 |
3 | Create Product
4 |
5 |
6 |
7 |
8 | | id |
9 | name |
10 | categpory |
11 | description |
12 | images count |
13 | certificate |
14 | edit |
15 | delete |
16 |
17 |
18 |
19 | | id ?> |
20 | name ?> |
21 | category ? $product->category->name : '' ?> |
22 | description ? $product->description->color . '/' . $product->description->size : '' ?> |
23 | images ? count($product->images) : '' ?> |
24 | certificate ? $product->certificate->name : '' ?> |
25 | Edit |
26 | Delete |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/tests/fixtures/models/ProductForm.php:
--------------------------------------------------------------------------------
1 |
4 | * @link http://weavora.com
5 | * @copyright Copyright (c) 2011 Weavora LLC
6 | */
7 |
8 | class ProductForm extends Product
9 | {
10 |
11 | public static function model($className=__CLASS__)
12 | {
13 | return parent::model($className);
14 | }
15 |
16 | public function relations()
17 | {
18 | return array_merge(parent::relations(), array(
19 | 'images' => array(self::HAS_MANY, 'AttachmentForm', 'object_id', 'condition' => 'images.object_type=:object_type', 'params' => array('object_type' => Attachment::OBJECT_TYPE_PRODUCT_IMAGE)),
20 | 'certificate' => array(self::HAS_ONE, 'CertificateForm', 'product_id'),
21 | ));
22 | }
23 |
24 |
25 | public function behaviors()
26 | {
27 | return array_merge(
28 | parent::behaviors(),
29 | array(
30 | 'wform' => array(
31 | 'class' => 'ext.wform.WFormBehavior',
32 | 'relations' => array(
33 | 'category' => array('unsetInvalid' => true, 'required' => false),
34 | 'tags' => array('required' => false),
35 | 'images',
36 | 'certificate',
37 | 'description',
38 | ),
39 | ),
40 | )
41 | );
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/WFormRelationBelongsTo.php:
--------------------------------------------------------------------------------
1 |
4 | * @link http://weavora.com
5 | * @copyright Copyright (c) 2011 Weavora LLC
6 | */
7 |
8 | class WFormRelationBelongsTo extends WFormRelationHasOne {
9 |
10 | public $type = CActiveRecord::BELONGS_TO;
11 |
12 | public $required = true;
13 |
14 | public function save() {
15 | $relationModel = $this->getRelatedModel($this->required);
16 |
17 | if (is_null($relationModel) && !$this->required)
18 | return true;
19 |
20 | if (!$relationModel->save())
21 | return false;
22 |
23 | $foreignKey = $this->info[WFormRelation::RELATION_FOREIGN_KEY];
24 | $this->model->{$foreignKey} = $relationModel->primaryKey;
25 |
26 | return true;
27 | }
28 |
29 | public function getRelatedModel($createNewIfEmpty = true) {
30 | $relationClass = $this->info[WFormRelation::RELATION_CLASS];
31 |
32 | if (!$this->model->{$this->name} || (!$this->isAttributesPerformed() && $this->isPreloaded())) {
33 | if (!$createNewIfEmpty)
34 | return null;
35 |
36 | $this->model->{$this->name} = new $relationClass();
37 | }
38 |
39 | return $this->model->{$this->name};
40 | }
41 |
42 | public function delete() {
43 | return true;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/examples/product/models/forms/ProductForm.php:
--------------------------------------------------------------------------------
1 |
4 | * @link http://weavora.com
5 | * @copyright Copyright (c) 2011 Weavora LLC
6 | */
7 |
8 | class ProductForm extends Product
9 | {
10 |
11 | public static function model($className=__CLASS__)
12 | {
13 | return parent::model($className);
14 | }
15 |
16 | public function relations()
17 | {
18 | return array_merge(parent::relations(), array(
19 | 'images' => array(self::HAS_MANY, 'AttachmentForm', 'object_id',
20 | 'condition' => 'images.object_type = :object_type',
21 | 'params' => array('object_type' => Attachment::OBJECT_TYPE_PRODUCT_IMAGE),
22 | 'together' => false,
23 | ),
24 | 'certificate' => array(self::HAS_ONE, 'CertificateForm', 'product_id'),
25 | ));
26 | }
27 |
28 |
29 | public function behaviors()
30 | {
31 | return array_merge(
32 | parent::behaviors(),
33 | array(
34 | 'wform' => array(
35 | 'class' => 'ext.wform.WFormBehavior',
36 | 'relations' => array(
37 | 'category' => array('unsetInvalid' => true, 'required' => false),
38 | 'tags' => array('required' => false),
39 | 'images',
40 | 'certificate',
41 | 'description',
42 | ),
43 | ),
44 | )
45 | );
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/tests/fixtures/models/Product.php:
--------------------------------------------------------------------------------
1 |
4 | * @link http://weavora.com
5 | * @copyright Copyright (c) 2011 Weavora LLC
6 | */
7 |
8 | class Product extends WActiveRecord
9 | {
10 |
11 | public static function model($className=__CLASS__)
12 | {
13 | return parent::model($className);
14 | }
15 |
16 | public function tableName()
17 | {
18 | return 'products';
19 | }
20 |
21 | public function rules()
22 | {
23 | return array(
24 | array('price, category_id', 'numerical'),
25 | array('name', 'required'),
26 | );
27 | }
28 |
29 | public function relations()
30 | {
31 | return array(
32 | 'category' => array(self::BELONGS_TO, 'Category', 'category_id'),
33 | 'tags' => array(self::MANY_MANY, 'Tag', 'products_2_tags(product_id, tag_id)'),
34 | 'images' => array(self::HAS_MANY, 'Attachment', 'object_id', 'condition' => 'images.object_type=:object_type', 'params' => array('object_type' => Attachment::OBJECT_TYPE_PRODUCT_IMAGE)),
35 | 'certificate' => array(self::HAS_ONE, 'Certificate', 'product_id'),
36 | 'description' => array(self::HAS_ONE, 'ProductDescription', 'product_id'),
37 | );
38 | }
39 |
40 | public function attributeLabels()
41 | {
42 | return array(
43 | 'id' => 'ID',
44 | 'name' => 'Name',
45 | 'price' => 'Price',
46 | );
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/examples/product/controllers/ProductController.php:
--------------------------------------------------------------------------------
1 |
4 | * @link http://weavora.com
5 | * @copyright Copyright (c) 2011 Weavora LLC
6 | */
7 |
8 | class ProductController extends Controller
9 | {
10 | public function actionIndex()
11 | {
12 | $products = ProductForm::model()->findAll();
13 | $this->render('index', array(
14 | 'products' => $products
15 | ));
16 | }
17 |
18 | public function actionAdd()
19 | {
20 | $this->forward('edit');
21 | }
22 |
23 | public function actionEdit($id = null)
24 | {
25 | $productForm = $id ? ProductForm::model()->with('images','tags')->findByPk($id) : new ProductForm();
26 |
27 | if (Yii::app()->request->getPost('ProductForm')) {
28 | $productForm->attributes = Yii::app()->request->getPost('ProductForm');
29 | if ($productForm->save()) {
30 | $this->redirect($this->createUrl('product/index'));
31 | }
32 | }
33 | $this->render('edit', array(
34 | 'product' => $productForm,
35 | 'categories' => Category::model()->findAll(),
36 | 'tags' => Tag::model()->findAll()
37 | ));
38 | }
39 |
40 | public function actionDelete($id)
41 | {
42 | $productForm = ProductForm::model()->findByPk($id);
43 | if (!empty($productForm)) {
44 | $productForm->delete();
45 | }
46 | $this->redirect($this->createUrl('product/index'));
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/examples/product/models/Attachment.php:
--------------------------------------------------------------------------------
1 |
4 | * @link http://weavora.com
5 | * @copyright Copyright (c) 2011 Weavora LLC
6 | */
7 |
8 | class Attachment extends WActiveRecord
9 | {
10 |
11 | const OBJECT_TYPE_PRODUCT_IMAGE = 'product_image';
12 | const OBJECT_TYPE_CERTIFICATE = 'certificate';
13 |
14 | public static function model($className=__CLASS__)
15 | {
16 | return parent::model($className);
17 | }
18 |
19 | public function tableName()
20 | {
21 | return 'attachments';
22 | }
23 |
24 | public function rules()
25 | {
26 | return array(
27 | array('object_id', 'numerical', 'integerOnly'=>true),
28 | array('object_type', 'length', 'max'=>13),
29 | array('file, file_origin', 'length', 'max'=>250),
30 | );
31 | }
32 |
33 | public function attributeLabels()
34 | {
35 | return array(
36 | 'id' => 'ID',
37 | 'object_id' => 'Object',
38 | 'object_type' => 'Object Type',
39 | 'file' => 'File',
40 | 'file_origin' => 'File Origin',
41 | );
42 | }
43 |
44 | public function getFilePath()
45 | {
46 | return Yii::app()->runtimePath.'/'.$this->object_type . '/' . $this->file;
47 | }
48 |
49 | public function getFileUrl()
50 | {
51 | if ($this->file) {
52 | return Yii::app()->createUrl('/protected/runtime/' . $this->object_type . '/' . $this->file);
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/examples/product/models/Product.php:
--------------------------------------------------------------------------------
1 |
4 | * @link http://weavora.com
5 | * @copyright Copyright (c) 2011 Weavora LLC
6 | */
7 |
8 | class Product extends WActiveRecord
9 | {
10 |
11 | public static function model($className=__CLASS__)
12 | {
13 | return parent::model($className);
14 | }
15 |
16 | public function tableName()
17 | {
18 | return 'products';
19 | }
20 |
21 | public function rules()
22 | {
23 | return array(
24 | array('price, category_id', 'numerical'),
25 | array('name', 'required'),
26 | array('name', 'length', 'max'=>200),
27 | );
28 | }
29 |
30 | public function relations()
31 | {
32 | return array(
33 | 'category' => array(self::BELONGS_TO, 'Category', 'category_id'),
34 | 'tags' => array(self::MANY_MANY, 'Tag', 'products_2_tags(product_id, tag_id)'),
35 | 'images' => array(self::HAS_MANY, 'Attachment', 'object_id',
36 | 'condition' => 'images.object_type=:object_type',
37 | 'params' => array('object_type' => Attachment::OBJECT_TYPE_PRODUCT_IMAGE),
38 | 'together' => false,
39 | ),
40 | 'certificate' => array(self::HAS_ONE, 'Certificate', 'product_id'),
41 | 'description' => array(self::HAS_ONE, 'ProductDescription', 'product_id'),
42 | );
43 | }
44 |
45 | public function attributeLabels()
46 | {
47 | return array(
48 | 'id' => 'ID',
49 | 'name' => 'Name',
50 | 'price' => 'Price',
51 | );
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/tests/unit/WFileIteratorTest.php:
--------------------------------------------------------------------------------
1 | object = new WFileIterator;
21 | }
22 |
23 | /**
24 | * Tears down the fixture, for example, closes a network connection.
25 | * This method is called after a test is executed.
26 | */
27 | protected function tearDown()
28 | {
29 | }
30 |
31 | /**
32 | * @covers {className}::{origMethodName}
33 | * @todo Implement testGetFiles().
34 | */
35 | public function testGetFiles()
36 | {
37 | // Remove the following lines when you implement this test.
38 | $this->markTestIncomplete(
39 | 'This test has not been implemented yet.'
40 | );
41 | }
42 |
43 | /**
44 | * @covers {className}::{origMethodName}
45 | * @todo Implement test_toPaths().
46 | */
47 | public function test_toPaths()
48 | {
49 | // Remove the following lines when you implement this test.
50 | $this->markTestIncomplete(
51 | 'This test has not been implemented yet.'
52 | );
53 | }
54 | }
55 | ?>
56 |
--------------------------------------------------------------------------------
/tests/unit/WActiveRecordTest.php:
--------------------------------------------------------------------------------
1 | markTestSkipped('PDO and SQLite extensions are required.');
16 |
17 | $this->_connection = new CDbConnection('sqlite::memory:');
18 | $this->_connection->active = true;
19 | $this->_connection->pdoInstance->exec(file_get_contents(dirname(__FILE__).'/../fixtures/data/sqlite.sql'));
20 | CActiveRecord::$db = $this->_connection;
21 | }
22 |
23 |
24 | protected function tearDown()
25 | {
26 | $this->_connection->active=false;
27 | }
28 |
29 | /**
30 | * @covers WActiveRecord::onUnsafeAttribute
31 | */
32 | public function testOnUnsafeAttribute()
33 | {
34 | $unsafeAttributes = array();
35 | $product = new Product();
36 | $product->onUnsafeAttribute = function($event) use (&$unsafeAttributes) {
37 | $unsafeAttributes[] = $event->params;
38 | };
39 | $product->attributes = array(
40 | 'name' => 'product name',
41 | 'images' => array(
42 | 'file' => 'file.txt'
43 | ),
44 | 'tags' => array(),
45 | );
46 |
47 | $this->assertNotEmpty($unsafeAttributes);
48 | $this->assertCount(2, $unsafeAttributes);
49 | $this->assertEquals($unsafeAttributes[0]['name'], 'images');
50 | $this->assertCount(1, $unsafeAttributes[0]['value']);
51 | $this->assertEquals($unsafeAttributes[0]['value']['file'], 'file.txt');
52 | $this->assertEquals($unsafeAttributes[1]['name'], 'tags');
53 | $this->assertEmpty($unsafeAttributes[1]['value']);
54 | }
55 |
56 | }
57 |
--------------------------------------------------------------------------------
/WFileIterator.php:
--------------------------------------------------------------------------------
1 |
4 | * @link http://weavora.com
5 | * @copyright Copyright (c) 2011 Weavora LLC
6 | */
7 |
8 | class WFileIterator extends CMap {
9 |
10 | public function __construct($model) {
11 | $modelClass = get_class($model);
12 | $files = $this->getFiles($modelClass);
13 | $this->copyFrom($files);
14 | }
15 |
16 | public function getFiles($key) {
17 | if (!isset($_FILES[$key])) {
18 | return array();
19 | }
20 |
21 | $files = $this->_normalize($_FILES[$key]);
22 |
23 | return $this->_toPaths($files);
24 | }
25 |
26 | public function _toPaths($files) {
27 | $complete = false;
28 | while(!$complete) {
29 | $complete = true;
30 | foreach($files as $key => $file) {
31 | if (!($file instanceof CUploadedFile)) {
32 | if (!$this->_isFile($file)) {
33 | $complete = false;
34 | if (is_array($file)) {
35 | foreach($file as $subKey => $subFile) {
36 | $files[$key . '.' . $subKey] = $subFile;
37 | }
38 | }
39 | unset($files[$key]);
40 | } elseif ($file['error'] != UPLOAD_ERR_OK) {
41 | unset($files[$key]);
42 | } else {
43 | $files[$key] = new CUploadedFile($file['name'], $file['tmp_name'], $file['type'], $file['size'], $file['error']);
44 | }
45 | }
46 | }
47 | }
48 | return $files;
49 | }
50 |
51 | protected function _normalize($files = array()) {
52 | $normalizedFiles = array();
53 | foreach($files as $key => $value) {
54 | if (is_array($value)) {
55 | foreach($value as $k=>$v) {
56 | $normalizedFiles[$k][$key] = $v;
57 | }
58 | } else {
59 | $normalizedFiles[$key] = $value;
60 | }
61 | }
62 |
63 | foreach($normalizedFiles as $k => $v) {
64 | if (is_array($v))
65 | $normalizedFiles[$k] = $this->_normalize($v);
66 | }
67 |
68 | return $normalizedFiles;
69 | }
70 |
71 | protected function _isFile($data) {
72 | return isset($data['name']) && isset($data['tmp_name']) && isset($data['size']) && isset($data['error']);
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/tests/fixtures/models/AttachmentForm.php:
--------------------------------------------------------------------------------
1 |
4 | * @link http://weavora.com
5 | * @copyright Copyright (c) 2011 Weavora LLC
6 | */
7 |
8 | class AttachmentForm extends Attachment
9 | {
10 |
11 | public $tempFile = null;
12 |
13 | public static function model($className=__CLASS__)
14 | {
15 | return parent::model($className);
16 | }
17 |
18 | public function rules()
19 | {
20 | return array_merge(parent::rules(), array(
21 | array('tempFile', 'safe'),
22 | ));
23 | }
24 |
25 | public static function create($type)
26 | {
27 | $attachmentForm = new AttachmentForm();
28 | $attachmentForm->object_type = $type;
29 | return $attachmentForm;
30 | }
31 |
32 | public function beforeValidate()
33 | {
34 | if ($this->file instanceof CUploadedFile) {
35 | // save to tmp folder
36 | $tempFile = new WTempFile(Yii::app()->runtimePath);
37 |
38 | if ($this->file->saveAs($tempFile->getPath())) {
39 | $this->tempFile = $tempFile->getFile();
40 |
41 | // setup proper file_origin
42 | $this->file_origin = $this->file->getName();
43 | }
44 | }
45 | return true;
46 | }
47 |
48 | public function saveUploadedFile()
49 | {
50 | if (empty($this->file_origin)) {
51 | if (!$this->isNewRecord)
52 | $this->delete();
53 | return false;
54 | }
55 |
56 | if (empty($this->tempFile)) {
57 | return false;
58 | }
59 |
60 | $tempFile = new WTempFile(Yii::app()->runtimePath);
61 | $tempFile->setFile($this->tempFile);
62 |
63 | if (!$tempFile->isValid()) {
64 | return false;
65 | }
66 |
67 | $attachmentDirectory = Yii::app()->runtimePath . '/' . $this->object_type . '/';
68 |
69 | if (!is_dir($attachmentDirectory)) {
70 | mkdir($attachmentDirectory);
71 | }
72 |
73 | $fileName = $this->id . '.' . pathinfo($this->file_origin, PATHINFO_EXTENSION);
74 |
75 |
76 | if ($tempFile->saveAs($attachmentDirectory . $fileName)) {
77 | $this->file = $fileName;
78 | $this->isNewRecord = false;
79 | $this->tempFile = null;
80 | $this->save(false);
81 | }
82 |
83 | $this->tempFile = null;
84 |
85 | return false;
86 | }
87 |
88 | public function afterSave()
89 | {
90 | $this->saveUploadedFile();
91 | return parent::afterSave();
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/examples/product/models/forms/AttachmentForm.php:
--------------------------------------------------------------------------------
1 |
4 | * @link http://weavora.com
5 | * @copyright Copyright (c) 2011 Weavora LLC
6 | */
7 |
8 | class AttachmentForm extends Attachment
9 | {
10 |
11 | public $tempFile = null;
12 |
13 | public static function model($className=__CLASS__)
14 | {
15 | return parent::model($className);
16 | }
17 |
18 | public function rules()
19 | {
20 | return array_merge(parent::rules(), array(
21 | array('tempFile', 'safe'),
22 | ));
23 | }
24 |
25 | public static function create($type)
26 | {
27 | $attachmentForm = new AttachmentForm();
28 | $attachmentForm->object_type = $type;
29 | return $attachmentForm;
30 | }
31 |
32 | public function beforeValidate()
33 | {
34 | if ($this->file instanceof CUploadedFile) {
35 | // save to tmp folder
36 | $tempFile = new WTempFile(Yii::app()->runtimePath);
37 |
38 | if ($this->file->saveAs($tempFile->getPath())) {
39 | $this->tempFile = $tempFile->getFile();
40 |
41 | // setup proper file_origin
42 | $this->file_origin = $this->file->getName();
43 | }
44 | }
45 | return true;
46 | }
47 |
48 | public function saveUploadedFile()
49 | {
50 | if (empty($this->file_origin)) {
51 | if (!$this->isNewRecord)
52 | $this->delete();
53 | return false;
54 | }
55 |
56 | if (empty($this->tempFile)) {
57 | return false;
58 | }
59 |
60 | $tempFile = new WTempFile(Yii::app()->runtimePath);
61 | $tempFile->setFile($this->tempFile);
62 |
63 | if (!$tempFile->isValid()) {
64 | return false;
65 | }
66 |
67 | $attachmentDirectory = Yii::app()->runtimePath . '/' . $this->object_type . '/';
68 |
69 | if (!is_dir($attachmentDirectory)) {
70 | mkdir($attachmentDirectory);
71 | }
72 |
73 | $fileName = $this->id . '.' . pathinfo($this->file_origin, PATHINFO_EXTENSION);
74 |
75 |
76 | if ($tempFile->saveAs($attachmentDirectory . $fileName)) {
77 | $this->file = $fileName;
78 | $this->isNewRecord = false;
79 | $this->tempFile = null;
80 | $this->save(false);
81 | }
82 |
83 | $this->tempFile = null;
84 |
85 | return false;
86 | }
87 |
88 | public function afterSave()
89 | {
90 | $this->saveUploadedFile();
91 | return parent::afterSave();
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/WFormRelationHasOne.php:
--------------------------------------------------------------------------------
1 |
4 | * @link http://weavora.com
5 | * @copyright Copyright (c) 2011 Weavora LLC
6 | */
7 |
8 | class WFormRelationHasOne extends WFormRelation {
9 |
10 | public $type = CActiveRecord::HAS_ONE;
11 |
12 | public $required = true;
13 |
14 | public function setAttributes($attributes) {
15 | parent::setAttributes($attributes);
16 |
17 | if (!is_null($attributes)) {
18 | $relationModel = $this->getRelatedModel();
19 | $relationModel->attributes = $attributes;
20 | } else {
21 | $this->model->{$this->name} = null;
22 | }
23 | }
24 |
25 | public function validate() {
26 | $relationModel = $this->getRelatedModel($this->required);
27 |
28 | if (is_null($relationModel) && !$this->required)
29 | return true;
30 |
31 |
32 | return $relationModel->validate();
33 | }
34 |
35 | public function save() {
36 | $relationModel = $this->getRelatedModel($this->required);
37 |
38 | if ($this->mode == self::MODE_REPLACE && ($actualModel = $this->getActualRelatedModel()) !== null) {
39 | $this->addToLazyDelete($actualModel);
40 | }
41 |
42 | if (is_null($relationModel) && !$this->required)
43 | return true;
44 |
45 | $foreignKey = $this->info[WFormRelation::RELATION_FOREIGN_KEY];
46 | $relationModel->$foreignKey = $this->model->primaryKey;
47 |
48 | $this->removeFromLazyDelete($relationModel);
49 |
50 | return $relationModel->save();
51 | }
52 |
53 | public function getRelatedModel($createNewIfEmpty = true) {
54 | $relationClass = $this->info[WFormRelation::RELATION_CLASS];
55 |
56 | if (!$this->model->{$this->name} || (!$this->isAttributesPerformed() && $this->isPreloaded())) {
57 | $this->model->{$this->name} = $createNewIfEmpty ? new $relationClass() : null;
58 | }
59 |
60 | return $this->model->{$this->name};
61 | }
62 |
63 | public function getActualRelatedModel() {
64 | if ($this->model->isNewRecord)
65 | return null;
66 |
67 | $modelClone = clone $this->model;
68 | return $modelClone->getRelated($this->name, true);
69 | }
70 |
71 | public function delete() {
72 | if (!$this->cascadeDelete)
73 | return true;
74 |
75 | $model = $this->getActualRelatedModel();
76 | if ($model)
77 | return $model->delete();
78 |
79 | return true;
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/tests/unit/WTempFileTest.php:
--------------------------------------------------------------------------------
1 | markTestIncomplete(
36 | 'This test has not been implemented yet.'
37 | );
38 | }
39 |
40 | /**
41 | * @covers {className}::{origMethodName}
42 | * @todo Implement testIsValid().
43 | */
44 | public function testIsValid()
45 | {
46 | // Remove the following lines when you implement this test.
47 | $this->markTestIncomplete(
48 | 'This test has not been implemented yet.'
49 | );
50 | }
51 |
52 | /**
53 | * @covers {className}::{origMethodName}
54 | * @todo Implement testGetPath().
55 | */
56 | public function testGetPath()
57 | {
58 | // Remove the following lines when you implement this test.
59 | $this->markTestIncomplete(
60 | 'This test has not been implemented yet.'
61 | );
62 | }
63 |
64 | /**
65 | * @covers {className}::{origMethodName}
66 | * @todo Implement testSaveAs().
67 | */
68 | public function testSaveAs()
69 | {
70 | // Remove the following lines when you implement this test.
71 | $this->markTestIncomplete(
72 | 'This test has not been implemented yet.'
73 | );
74 | }
75 |
76 | /**
77 | * @covers {className}::{origMethodName}
78 | * @todo Implement testUpload().
79 | */
80 | public function testUpload()
81 | {
82 | // Remove the following lines when you implement this test.
83 | $this->markTestIncomplete(
84 | 'This test has not been implemented yet.'
85 | );
86 | }
87 |
88 | /**
89 | * @covers {className}::{origMethodName}
90 | * @todo Implement testGetFile().
91 | */
92 | public function testGetFile()
93 | {
94 | // Remove the following lines when you implement this test.
95 | $this->markTestIncomplete(
96 | 'This test has not been implemented yet.'
97 | );
98 | }
99 | }
--------------------------------------------------------------------------------
/tests/fixtures/data/sqlite.sql:
--------------------------------------------------------------------------------
1 | DROP TABLE IF EXISTS `attachments`;
2 | CREATE TABLE `attachments` (
3 | `id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
4 | `object_id` INTEGER,
5 | `object_type` VARCHAR(250),
6 | `file` varchar(250),
7 | `file_origin` varchar(250)
8 | );
9 |
10 | -- ----------------------------
11 | -- Table structure for categories
12 | -- ----------------------------
13 | DROP TABLE IF EXISTS `categories`;
14 | CREATE TABLE `categories` (
15 | `id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
16 | `name` varchar(200)
17 | );
18 |
19 | -- ----------------------------
20 | -- Table structure for certificates
21 | -- ----------------------------
22 | DROP TABLE IF EXISTS `certificates`;
23 | CREATE TABLE `certificates` (
24 | `id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
25 | `product_id` int(11),
26 | `name` varchar(11)
27 | );
28 |
29 | -- ----------------------------
30 | -- Table structure for product_descriptions
31 | -- ----------------------------
32 | DROP TABLE IF EXISTS `product_descriptions`;
33 | CREATE TABLE `product_descriptions` (
34 | `id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
35 | `product_id` int(11) NOT NULL,
36 | `color` varchar(200),
37 | `size` varchar(200)
38 | );
39 |
40 | -- ----------------------------
41 | -- Table structure for products
42 | -- ----------------------------
43 | DROP TABLE IF EXISTS `products`;
44 | CREATE TABLE `products` (
45 | `id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
46 | `category_id` int(11),
47 | `name` varchar(200),
48 | `price` double
49 | );
50 |
51 | -- ----------------------------
52 | -- Table structure for products_2_tags
53 | -- ----------------------------
54 | DROP TABLE IF EXISTS `products_2_tags`;
55 | CREATE TABLE `products_2_tags` (
56 | `product_id` INTEGER NOT NULL,
57 | `tag_id` INTEGER NOT NULL,
58 | PRIMARY KEY (`product_id`,`tag_id`)
59 | );
60 |
61 | -- ----------------------------
62 | -- Table structure for tags
63 | -- ----------------------------
64 | DROP TABLE IF EXISTS `tags`;
65 | CREATE TABLE `tags` (
66 | `id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
67 | `name` varchar(200)
68 | );
69 |
70 | -- ----------------------------
71 | -- Records
72 | -- ----------------------------
73 | INSERT INTO `categories` VALUES ('1', 'Auto');
74 | INSERT INTO `categories` VALUES ('2', 'Mobile');
75 | INSERT INTO `categories` VALUES ('3', 'Used');
76 |
77 | INSERT INTO `tags` VALUES ('1', 'bad');
78 | INSERT INTO `tags` VALUES ('2', 'good');
79 | INSERT INTO `tags` VALUES ('3', 'awesome');
80 |
81 | INSERT INTO `certificates` VALUES ('1', '1', '9045');
82 | INSERT INTO `product_descriptions` VALUES ('1', '1', 'Red', '100x100');
83 | INSERT INTO `products` VALUES ('1', '1', 'Test Product', '99');
84 | INSERT INTO `attachments` VALUES ('1', '1', 'product_image', 'file.txt', '/path/to/file.txt');
85 |
86 |
87 | INSERT INTO `products_2_tags` VALUES ('1', '2');
88 | INSERT INTO `products_2_tags` VALUES ('1', '3');
--------------------------------------------------------------------------------
/examples/product/product.sql:
--------------------------------------------------------------------------------
1 | -- ----------------------------
2 | -- Table structure for attachments
3 | -- ----------------------------
4 | CREATE TABLE `attachments` (
5 | `id` int(11) unsigned NOT NULL auto_increment,
6 | `object_id` int(11) default NULL,
7 | `object_type` enum('product_image','certificate') default NULL,
8 | `file` varchar(250) default NULL,
9 | `file_origin` varchar(250) default NULL,
10 | PRIMARY KEY (`id`)
11 | ) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
12 |
13 | -- ----------------------------
14 | -- Table structure for categories
15 | -- ----------------------------
16 | CREATE TABLE `categories` (
17 | `id` int(11) unsigned NOT NULL auto_increment,
18 | `name` varchar(200) default NULL,
19 | PRIMARY KEY (`id`)
20 | ) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
21 |
22 | -- ----------------------------
23 | -- Table structure for certificates
24 | -- ----------------------------
25 | CREATE TABLE `certificates` (
26 | `id` int(11) unsigned NOT NULL auto_increment,
27 | `product_id` int(11) default NULL,
28 | `name` varchar(11) default NULL,
29 | PRIMARY KEY (`id`)
30 | ) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
31 |
32 | -- ----------------------------
33 | -- Table structure for product_descriptions
34 | -- ----------------------------
35 | CREATE TABLE `product_descriptions` (
36 | `id` int(11) unsigned NOT NULL auto_increment,
37 | `product_id` int(11) NOT NULL,
38 | `color` varchar(200) default NULL,
39 | `size` varchar(200) default NULL,
40 | PRIMARY KEY (`id`)
41 | ) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
42 |
43 | -- ----------------------------
44 | -- Table structure for products
45 | -- ----------------------------
46 | CREATE TABLE `products` (
47 | `id` int(11) unsigned NOT NULL auto_increment,
48 | `category_id` int(11) default NULL,
49 | `name` varchar(200) default NULL,
50 | `price` double default NULL,
51 | PRIMARY KEY (`id`)
52 | ) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
53 |
54 | -- ----------------------------
55 | -- Table structure for products_2_tags
56 | -- ----------------------------
57 | CREATE TABLE `products_2_tags` (
58 | `product_id` int(11) unsigned NOT NULL,
59 | `tag_id` int(11) unsigned NOT NULL,
60 | PRIMARY KEY (`product_id`,`tag_id`)
61 | ) ENGINE=MyISAM DEFAULT CHARSET=utf8;
62 |
63 | -- ----------------------------
64 | -- Table structure for tags
65 | -- ----------------------------
66 | CREATE TABLE `tags` (
67 | `id` int(11) unsigned NOT NULL auto_increment,
68 | `name` varchar(200) default NULL,
69 | PRIMARY KEY (`id`)
70 | ) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
71 |
72 | -- ----------------------------
73 | -- Records
74 | -- ----------------------------
75 | INSERT INTO `categories` VALUES ('1', 'Auto');
76 | INSERT INTO `categories` VALUES ('2', 'Mobile');
77 | INSERT INTO `categories` VALUES ('3', 'Used');
78 | INSERT INTO `certificates` VALUES ('1', '1', '9045');
79 | INSERT INTO `product_descriptions` VALUES ('1', '1', 'Red', '100x100');
80 | INSERT INTO `products` VALUES ('1', '1', 'Test Product', '99');
81 | INSERT INTO `attachments` VALUES ('1', '1', 'product_image', 'file.text', 'path/to/file.txt');
82 | INSERT INTO `products_2_tags` VALUES ('1', '2');
83 | INSERT INTO `products_2_tags` VALUES ('1', '3');
84 | INSERT INTO `tags` VALUES ('1', 'bad');
85 | INSERT INTO `tags` VALUES ('2', 'good');
86 | INSERT INTO `tags` VALUES ('3', 'awesome');
--------------------------------------------------------------------------------
/js/jquery.multiplyforms.js:
--------------------------------------------------------------------------------
1 | (function($){
2 | $.multiplyForms = function(element, options){
3 |
4 | var self = this;
5 | self.element = $(element);
6 |
7 | //self.formCounter = 100;
8 | self.options = $.extend({}, $.multiplyForms.defaultOptions, options);
9 |
10 | self.element.data("multiplyForms", self);
11 | self.template = self.element.find("." + self.options.templateClass);
12 |
13 | self.init = function() {
14 |
15 | self.template.find('input, textarea, select, button').attr('disabled', 'disabled');
16 |
17 | // find add links inside self.element
18 | var addLinks = self.element.find(self.options.addLink);
19 | // find add links in document
20 | addLinks = addLinks.length ? addLinks : $(self.options.addLink);
21 |
22 | addLinks.click(function(e) {
23 | e.preventDefault();
24 | self._cloneTemplate();
25 | });
26 |
27 | self.element.find(self.options.deleteLink).live('click', function(e) {
28 | e.preventDefault();
29 |
30 | var embedForm = $(e.target).parents("." + self.options.embedClass);
31 |
32 | // beforeDelete callback
33 | var e = jQuery.Event("multiplyForms.delete", {multiplyFormInstance: self});
34 | e.target = embedForm;
35 | self.element.trigger(e, [embedForm, self]);
36 | // if ($.isFunction(self.options.beforeDelete)) {
37 | // self.options.beforeDelete.call(this, embedForm, self);
38 | // }
39 | if (!e.isDefaultPrevented())
40 | embedForm.remove();
41 | });
42 | };
43 |
44 | self._cloneTemplate = function() {
45 | var self = this;
46 | var newForm = self.template
47 | .clone(false)
48 | .find('input, textarea, select, button')
49 | .removeAttr('disabled')
50 | .end();
51 |
52 | if (self.options.mode == "append") {
53 | if (self.options.appendTo) {
54 | self.element.find(self.options.appendTo).append(newForm);
55 | } else {
56 | newForm.appendTo(self.element);
57 | }
58 | } else {
59 | if (self.options.prependTo) {
60 | self.element.find(self.options.prependTo).append(newForm);
61 | } else {
62 | newForm.prependTo(self.element);
63 | }
64 | }
65 |
66 | newForm
67 | .addClass(self.options.embedClass)
68 | .removeClass(self.options.templateClass)
69 | .show();
70 |
71 | self._updateIndex(newForm);
72 |
73 |
74 | // afterAdd callback
75 | var e = jQuery.Event("multiplyForms.add");
76 | e.target = newForm;
77 | self.element.trigger(e, [newForm, self]);
78 | // if ($.isFunction(self.options.afterAdd)) {
79 | // self.options.afterAdd.call(this, newForm, self);
80 | // }
81 | };
82 |
83 | self._updateIndex = function(form) {
84 | form.find('*[name*="{index}"]').each(function() {
85 | $(this).attr('name', $(this).attr('name').replace('{index}', $.multiplyForms.formCounter));
86 | this.id = this.id.replace('{index}', $.multiplyForms.formCounter);
87 | });
88 | $.multiplyForms.formCounter++;
89 | };
90 |
91 | self.init();
92 | };
93 |
94 | $.multiplyForms.defaultOptions = {
95 | addLink: ".add",
96 | deleteLink: ".delete",
97 | templateClass: "template",
98 | embedClass: "embed",
99 | afterAdd: undefined,
100 | beforeDelete: undefined,
101 | mode: "append",
102 | appendTo: undefined,
103 | prependTo: undefined
104 | };
105 |
106 | $.multiplyForms.formCounter = 1000;
107 |
108 | $.fn.multiplyForms = function(options) {
109 | return this.each(function(){
110 | (new $.multiplyForms(this, options));
111 | });
112 | };
113 |
114 | })(jQuery);
115 |
--------------------------------------------------------------------------------
/WFormRelationHasMany.php:
--------------------------------------------------------------------------------
1 |
4 | * @link http://weavora.com
5 | * @copyright Copyright (c) 2011 Weavora LLC
6 | */
7 |
8 | class WFormRelationHasMany extends WFormRelation {
9 |
10 | public $type = CActiveRecord::HAS_MANY;
11 |
12 | public function setAttributes($bunchOfAttributes) {
13 | parent::setAttributes($bunchOfAttributes);
14 |
15 | $relationClass = $this->info[WFormRelation::RELATION_CLASS];
16 | $relationPk = $relationClass::model()->getMetaData()->tableSchema->primaryKey;
17 |
18 | $modelsDictionary = array();
19 | foreach ($this->getRelatedModels() as $relationModel) {
20 | if ($relationModel->primaryKey) {
21 | $modelsDictionary[$relationModel->primaryKey] = $relationModel;
22 | }
23 | }
24 |
25 | $relationModels = array();
26 |
27 | foreach ($bunchOfAttributes as $key => &$attributes) {
28 | if (!is_array($attributes) && is_numeric($attributes)) {
29 | $attributes = array($relationPk => $attributes);
30 | }
31 |
32 | if (isset($attributes[$relationPk])) {
33 | if (isset($modelsDictionary[$attributes[$relationPk]])) {
34 | $relationModel = $modelsDictionary[$attributes[$relationPk]];
35 | } else {
36 | $relationModel = $relationClass::model()->findByPk($attributes[$relationPk]) ?: new $relationClass();
37 | }
38 | } else {
39 | $relationModel = new $relationClass();
40 | }
41 |
42 | $relationModel->attributes = $attributes;
43 | $relationModels[$key] = $relationModel;
44 | }
45 |
46 | $this->model->{$this->name} = $relationModels;
47 | }
48 |
49 | public function validate() {
50 | $isValid = true;
51 |
52 | $relatedModels = $this->getRelatedModels();
53 | if (count($relatedModels) == 0 && $this->required)
54 | return false;
55 |
56 | foreach ($relatedModels as $key => $relationModel) {
57 | if (!$relationModel->validate()) {
58 | if ($this->unsetInvalid) {
59 | unset($relatedModels[$key]);
60 | $this->model->{$this->name} = $relatedModels;
61 | } else {
62 | $isValid = false;
63 | }
64 | }
65 | }
66 | return $isValid;
67 | }
68 |
69 | public function save() {
70 | $foreignKey = $this->info[WFormRelation::RELATION_FOREIGN_KEY];
71 |
72 | if ($this->mode == self::MODE_REPLACE) {
73 | foreach($this->getActualRelatedModels() as $model)
74 | $this->addToLazyDelete($model);
75 | }
76 |
77 | $relatedModels = $this->getRelatedModels();
78 | if (count($relatedModels) == 0 && $this->required)
79 | return false;
80 |
81 | $isSuccess = true;
82 | foreach ($relatedModels as $index => $relationModel) {
83 | $this->removeFromLazyDelete($relationModel);
84 |
85 | $relationModel->$foreignKey = $this->model->primaryKey;
86 | $isSuccess = $relationModel->save() && $isSuccess;
87 | }
88 |
89 | return $isSuccess;
90 | }
91 |
92 | public function getRelatedModels() {
93 | if (!$this->model->{$this->name} || (!$this->isAttributesPerformed() && $this->isPreloaded())) {
94 | $this->model->{$this->name} = array();
95 | }
96 |
97 | return (array) $this->model->{$this->name};
98 | }
99 |
100 | public function getActualRelatedModels() {
101 | if ($this->model->isNewRecord)
102 | return array();
103 |
104 | $modelClone = clone $this->model;
105 | return (array) $modelClone->getRelated($this->name, true);
106 | }
107 |
108 | public function delete() {
109 | if (!$this->cascadeDelete)
110 | return true;
111 |
112 | $isSuccess = true;
113 | foreach($this->getActualRelatedModels() as $model)
114 | $isSuccess = $model->delete() && $isSuccess;
115 |
116 | return $isSuccess;
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/WFormRelationManyMany.php:
--------------------------------------------------------------------------------
1 |
4 | * @link http://weavora.com
5 | * @copyright Copyright (c) 2011 Weavora LLC
6 | */
7 |
8 | class WFormRelationManyMany extends WFormRelationHasMany {
9 |
10 | public $type = CActiveRecord::HAS_MANY;
11 |
12 | public function save() {
13 | if ($this->mode == self::MODE_REPLACE) {
14 | foreach($this->getActualRelatedModels() as $model)
15 | $this->addToLazyDelete($model);
16 | }
17 |
18 | $relatedModels = $this->getRelatedModels();
19 | if (count($relatedModels) == 0 && $this->required)
20 | return false;
21 |
22 | $isSuccess = true;
23 | foreach ($relatedModels as $index => $relationModel) {
24 | $this->removeFromLazyDelete($relationModel);
25 |
26 | if ($relationModel->save()) {
27 | $isSuccess = $this->_linkTo($relationModel) && $isSuccess;
28 | } else {
29 | $isSuccess = false;
30 | }
31 | }
32 |
33 | return $isSuccess;
34 | }
35 |
36 | /**
37 | * Insert link between parent and relation models into database
38 | *
39 | * @todo maybe we should execute bulk insert of links ? It faster a lot
40 | * @param $relatedModel
41 | * @return bool
42 | */
43 | protected function _linkTo($relatedModel) {
44 | $foreignKey = $this->_parseForeignKey($this->info[WFormRelation::RELATION_FOREIGN_KEY]);
45 |
46 | try {
47 | $sql = "INSERT INTO {$foreignKey['table']} ({$foreignKey['model_fk']}, {$foreignKey['relation_fk']}) VALUES (:model_fk,:relation_fk)";
48 |
49 | $command = $this->model->getDbConnection()->createCommand($sql);
50 | $command->bindValues(array(
51 | ":model_fk" => $this->model->primaryKey,
52 | ":relation_fk" => $relatedModel->primaryKey,
53 | ));
54 | $command->execute();
55 | } catch (Exception $e) {
56 | return false;
57 | }
58 | return true;
59 | }
60 |
61 | public function lazyDelete() {
62 | $relatedIds = array();
63 | foreach($this->_lazyDeleteRecords as $model) {
64 | $relatedIds[] = $model->primaryKey;
65 | }
66 |
67 |
68 | if (count($relatedIds))
69 | $this->_unlink($relatedIds);
70 | }
71 |
72 | public function delete() {
73 | if (!$this->cascadeDelete)
74 | return true;
75 |
76 | return $this->_unlink();
77 | }
78 |
79 | /**
80 | * Remove all links between parent and relation models into database
81 | *
82 | * @return bool
83 | */
84 | protected function _unlink($ids = null) {
85 | $foreignKey = $this->_parseForeignKey($this->info[WFormRelation::RELATION_FOREIGN_KEY]);
86 |
87 | try {
88 |
89 | $sql = "DELETE FROM {$foreignKey['table']} WHERE {$foreignKey['model_fk']} = :model_fk";
90 | if (!is_null($ids)) {
91 | $sql .= " AND {$foreignKey['relation_fk']} IN ('" . join("','", $ids) . "')";
92 | }
93 |
94 | $command = $this->model->getDbConnection()->createCommand($sql);
95 | $command->bindValues(array(
96 | ":model_fk" => $this->model->primaryKey,
97 | ));
98 | $command->execute();
99 |
100 | } catch (Exception $e) {
101 | return false;
102 | }
103 |
104 | return true;
105 | }
106 |
107 | /**
108 | * Parse foreign key into table name, model FK and relation FK
109 | *
110 | * @param $key
111 | * @return array
112 | */
113 | protected function _parseForeignKey($key) {
114 | if (preg_match('/(?P.*?)\((?P.*?),(?P.*?)\)/is', $key, $matches))
115 | {
116 | return array(
117 | 'table' => $this->model->getDbConnection()->quoteTableName(trim($matches['table'])),
118 | 'model_fk' => $this->model->getDbConnection()->quoteColumnName(trim($matches['model_fk'])),
119 | 'relation_fk' => $this->model->getDbConnection()->quoteColumnName(trim($matches['relation_fk'])),
120 | );
121 | }
122 |
123 | return null;
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/WFormRelation.php:
--------------------------------------------------------------------------------
1 |
4 | * @link http://weavora.com
5 | * @copyright Copyright (c) 2011 Weavora LLC
6 | */
7 |
8 | class WFormRelation {
9 |
10 | const RELATION_TYPE = 0;
11 | const RELATION_CLASS = 1;
12 | const RELATION_FOREIGN_KEY = 2;
13 |
14 | const MODE_APPEND = "append";
15 | const MODE_REPLACE = "replace";
16 |
17 | public $required = false;
18 | public $unsetInvalid = false;
19 | public $mode = self::MODE_REPLACE;
20 | public $cascadeDelete = true;
21 |
22 | // @todo why $name is public?
23 | public $name = null;
24 |
25 | // @todo rename to $_info, $_model, $_type
26 | protected $info = null;
27 | protected $model = null;
28 | protected $type = null;
29 |
30 | protected $_lazyDeleteRecords = array();
31 | protected $_attributesPerformed = false;
32 | protected $_preloaded = false; // relation was specified into CActiveRecord::with();
33 |
34 | public static function getInstance($model, $relationName, $options = array()) {
35 | // for 'relations' => array('someRelation','someOtherRelation')
36 | if (is_numeric($relationName) && is_string($options)) {
37 | $relationName = $options;
38 | $options = array();
39 | }
40 |
41 | $relationInfo = self::getRelationInfo($model, $relationName);
42 | if ($relationInfo === null)
43 | return null;
44 |
45 | switch($relationInfo[self::RELATION_TYPE]) {
46 | case CActiveRecord::HAS_ONE: $relation = new WFormRelationHasOne(); break;
47 | case CActiveRecord::HAS_MANY: $relation = new WFormRelationHasMany(); break;
48 | case CActiveRecord::BELONGS_TO: $relation = new WFormRelationBelongsTo(); break;
49 | case CActiveRecord::MANY_MANY: $relation = new WFormRelationManyMany(); break;
50 | default:
51 | return null;
52 |
53 | }
54 |
55 | $relation->setModel($model);
56 | $relation->setInfo($relationInfo);
57 | $relation->name = $relationName;
58 |
59 | $relation->setOptions($options);
60 |
61 | return $relation;
62 | }
63 |
64 | public function __construct($options = array()) {
65 | $this->setOptions($options);
66 | }
67 |
68 | public function setOptions($options) {
69 | foreach($options as $key => $value) {
70 | if (property_exists($this, $key) && !in_array($key, array('name', 'type', 'info', 'model'))) {
71 | $this->{$key} = $value;
72 | }
73 | }
74 | }
75 |
76 | public static function getRelationInfo($model, $relationName) {
77 | $relations = $model->relations();
78 | if (!array_key_exists($relationName, $relations))
79 | return null;
80 | return $relations[$relationName];
81 | }
82 |
83 | public function setInfo($info) {
84 | $this->info = $info;
85 | }
86 |
87 | public function setType($type) {
88 | $this->type = $type;
89 | }
90 |
91 | public function setModel($model) {
92 | $this->model = $model;
93 | }
94 |
95 | public function setPreloaded($isPreloaded) {
96 | $this->_preloaded = $isPreloaded;
97 | }
98 |
99 | public function isPreloaded() {
100 | return $this->_preloaded;
101 | }
102 |
103 | public function addToLazyDelete($model) {
104 | if (!$model->isNewRecord)
105 | $this->_lazyDeleteRecords[$model->primaryKey] = $model;
106 | }
107 |
108 | public function removeFromLazyDelete($model) {
109 | if (array_key_exists($model->primaryKey, $this->_lazyDeleteRecords))
110 | unset($this->_lazyDeleteRecords[$model->primaryKey]);
111 | }
112 |
113 | public function lazyDelete() {
114 | foreach($this->_lazyDeleteRecords as $model) {
115 | $model->delete();
116 | }
117 | }
118 |
119 | public function delete() {
120 | return true;
121 | }
122 |
123 | public function setAttributes($bunchOfAttributes) {
124 | $this->_attributesPerformed = true;
125 | }
126 |
127 | public function isAttributesPerformed() {
128 | return $this->_attributesPerformed;
129 | }
130 | }
--------------------------------------------------------------------------------
/tests/unit/WFormRelationTest.php:
--------------------------------------------------------------------------------
1 | getModelMock();
16 |
17 | $model->expects($this->any())
18 | ->method('setOptions');
19 |
20 | $options = array(
21 | 'required' => true,
22 | 'type' => 'someUnexpectedType'
23 | );
24 |
25 | // for 'relations' => array('someRelation' => array(..options..))
26 | $this->assertInstanceOf('WFormRelationHasOne', WFormRelation::getInstance($model, 'hasOne', $options));
27 | $this->assertInstanceOf('WFormRelationHasMany', WFormRelation::getInstance($model, 'hasMany', $options));
28 | $this->assertInstanceOf('WFormRelationManyMany', WFormRelation::getInstance($model, 'manyMany', $options));
29 | $this->assertInstanceOf('WFormRelationBelongsTo', WFormRelation::getInstance($model, 'belongsTo', $options));
30 | $this->assertNull(WFormRelation::getInstance($model, 'stat', $options));
31 | $this->assertNull(WFormRelation::getInstance($model, 'misteriousRelation', $options));
32 |
33 | // for 'relations' => array('someRelation','someOtherRelation')
34 | $this->assertInstanceOf('WFormRelationHasOne', WFormRelation::getInstance($model, 0, 'hasOne'));
35 | $this->assertInstanceOf('WFormRelationHasMany', WFormRelation::getInstance($model, 1, 'hasMany'));
36 | $this->assertInstanceOf('WFormRelationManyMany', WFormRelation::getInstance($model, 2, 'manyMany'));
37 | $this->assertInstanceOf('WFormRelationBelongsTo', WFormRelation::getInstance($model, 3, 'belongsTo'));
38 | $this->assertNull(WFormRelation::getInstance($model, 4, 'stat'));
39 | $this->assertNull(WFormRelation::getInstance($model, 5, 'misteriousRelation'));
40 |
41 | $relation = WFormRelation::getInstance($model, 'hasOne', $options);
42 |
43 | $this->assertAttributeNotEmpty('name', $relation);
44 | $this->assertAttributeNotEmpty('info', $relation);
45 | $this->assertAttributeNotEmpty('type', $relation);
46 | $this->assertAttributeNotEmpty('model', $relation);
47 | }
48 |
49 | /**
50 | * @covers WFormRelation::setOption
51 | */
52 | public function testSetOptions()
53 | {
54 | $relation = new WFormRelation();
55 |
56 | $relation->setOptions(array(
57 | 'required' => true,
58 | 'unsetInvalid' => true,
59 | 'name' => 'someName', // shouldn't affect
60 | 'info' => 'someInfo', // shouldn't affect
61 | 'type' => 'someType', // shouldn't affect
62 | 'model' => 'someModel', // shouldn't affect
63 | ));
64 |
65 | $this->assertTrue($relation->required);
66 | $this->assertTrue($relation->unsetInvalid);
67 |
68 | $this->assertAttributeEmpty('name', $relation);
69 | $this->assertAttributeEmpty('info', $relation);
70 | $this->assertAttributeEmpty('type', $relation);
71 | $this->assertAttributeEmpty('model', $relation);
72 |
73 | }
74 |
75 | /**
76 | * @covers WFormRelation::getRelationInfo
77 | */
78 | public function testGetRelationInfo()
79 | {
80 | $model = $this->getModelMock();
81 |
82 | $this->assertNotNull(WFormRelation::getRelationInfo($model, 'hasOne'));
83 | $this->assertNotNull(WFormRelation::getRelationInfo($model, 'hasMany'));
84 | $this->assertNotNull(WFormRelation::getRelationInfo($model, 'manyMany'));
85 | $this->assertNotNull(WFormRelation::getRelationInfo($model, 'belongsTo'));
86 | $this->assertNotNull(WFormRelation::getRelationInfo($model, 'stat'));
87 | $this->assertNull(WFormRelation::getRelationInfo($model, 'misteriousRelation'));
88 | }
89 |
90 | private function getModelMock() {
91 | $model = $this->getMockBuilder('CActiveRecord')
92 | ->disableOriginalConstructor()
93 | ->getMock();
94 |
95 | $model->expects($this->any())
96 | ->method('relations')
97 | ->will($this->returnValue(array(
98 | 'hasOne' => array(CActiveRecord::HAS_ONE, 'SomeModel', 'key'),
99 | 'hasMany' => array(CActiveRecord::HAS_MANY, 'SomeModel', 'key'),
100 | 'manyMany' => array(CActiveRecord::MANY_MANY, 'SomeModel', 'key'),
101 | 'belongsTo' => array(CActiveRecord::BELONGS_TO, 'SomeModel', 'key'),
102 | 'stat' => array(CActiveRecord::STAT, 'SomeModel', 'key'),
103 | )));
104 |
105 | return $model;
106 | }
107 | }
--------------------------------------------------------------------------------
/examples/product/README.md:
--------------------------------------------------------------------------------
1 | [Example source code](https://github.com/weavora/wform/tree/master/examples/product)
2 |
3 | # Setup
4 |
5 | 1. Copy the sample source to your application
6 | 2. Execute db dump from products.sql
7 | 3. Update imports in main.php to the following:
8 |
9 | ```php
10 | 'import'=>array(
11 | 'application.models.*',
12 | 'application.models.forms.*',
13 | 'application.components.*',
14 | 'ext.wform.*',
15 | ),
16 | ```
17 | 4. Now you have access to the sample controller:
18 |
19 | - Product List: http://yourhost.local/index.php?r=/product
20 | - Create a Product: http://yourhost.local/index.php?r=/product/add
21 |
22 | # Form Layout
23 |
24 | Here is what you should see:
25 |
26 | 
27 |
28 | # DB Diagram
29 |
30 | 
31 |
32 | # ProductController
33 |
34 | ```php
35 | forward('edit');
41 | }
42 |
43 | public function actionEdit($id = null)
44 | {
45 | $productForm = $id ? ProductForm::model()->findByPk($id) : new ProductForm();
46 |
47 | if (Yii::app()->request->getPost('ProductForm')) {
48 | $productForm ->attributes = Yii::app()->request->getPost('ProductForm');
49 | if ($productForm ->save()) {
50 | $this->redirect('/product/index');
51 | }
52 | }
53 | $this->render('edit', array(
54 | 'product' => $productForm ,
55 | 'categories' => Category::model()->findAll(),
56 | 'tags' => Tag::model()->findAll()
57 | ));
58 | }
59 | ...
60 | }
61 | ```
62 |
63 | # ProductForm
64 |
65 | ```php
66 | array(self::HAS_MANY, 'AttachmentForm', 'object_id', 'condition' => 'images.object_type=:object_type', 'params' => array('object_type' => Attachment::OBJECT_TYPE_PRODUCT_IMAGE)),
78 | 'certificate' => array(self::HAS_ONE, 'CertificateForm', 'product_id'),
79 | ));
80 | }
81 |
82 |
83 | public function behaviors()
84 | {
85 | return array_merge(
86 | parent::behaviors(),
87 | array(
88 | 'wform' => array(
89 | 'class' => 'ext.wform.WFormBehavior',
90 | 'relations' => array(
91 | 'category' => array('unsetInvalid' => true, 'required' => false),
92 | 'tags' => array('required' => false),
93 | 'images',
94 | 'certificate',
95 | 'description',
96 | ),
97 | ),
98 | )
99 | );
100 | }
101 | }
102 | ```
103 |
104 | # AttachmentForm
105 |
106 | ```php
107 |
108 | object_type = $type;
131 | return $attachmentForm;
132 | }
133 |
134 | public function beforeValidate()
135 | {
136 | if ($this->file instanceof CUploadedFile) {
137 | // save to tmp folder
138 | $tempFile = new WTempFile(Yii::app()->runtimePath);
139 |
140 | if ($this->file->saveAs($tempFile->getPath())) {
141 | $this->tempFile = $tempFile->getFile();
142 |
143 | // setup proper file_origin
144 | $this->file_origin = $this->file->getName();
145 | }
146 | }
147 | return true;
148 | }
149 |
150 | public function saveUploadedFile()
151 | {
152 | if (empty($this->file_origin)) {
153 | if (!$this->isNewRecord)
154 | $this->delete();
155 | return false;
156 | }
157 |
158 | if (empty($this->tempFile)) {
159 | return false;
160 | }
161 |
162 | $tempFile = new WTempFile(Yii::app()->runtimePath);
163 | $tempFile->setFile($this->tempFile);
164 |
165 | if (!$tempFile->isValid()) {
166 | return false;
167 | }
168 |
169 | $attachmentDirectory = Yii::app()->runtimePath . '/' . $this->object_type . '/';
170 |
171 | if (!is_dir($attachmentDirectory)) {
172 | mkdir($attachmentDirectory);
173 | }
174 |
175 | $fileName = $this->id . '.' . pathinfo($this->file_origin, PATHINFO_EXTENSION);
176 |
177 |
178 | if ($tempFile->saveAs($attachmentDirectory . $fileName)) {
179 | $this->file = $fileName;
180 | $this->isNewRecord = false;
181 | $this->tempFile = null;
182 | $this->save(false);
183 | }
184 |
185 | $this->tempFile = null;
186 |
187 | return false;
188 | }
189 |
190 | public function afterSave()
191 | {
192 | $this->saveUploadedFile();
193 | return parent::afterSave();
194 | }
195 | }
196 | ```
197 |
--------------------------------------------------------------------------------
/tests/unit/WFormBehaviorTest.php:
--------------------------------------------------------------------------------
1 | markTestSkipped('PDO and SQLite extensions are required.');
18 |
19 | $this->_connection = new CDbConnection('sqlite::memory:');
20 | $this->_connection->active = true;
21 | $this->_connection->pdoInstance->exec(file_get_contents(dirname(__FILE__) . '/../fixtures/data/sqlite.sql'));
22 | CActiveRecord::$db = $this->_connection;
23 | }
24 |
25 |
26 | protected function tearDown()
27 | {
28 | $this->_connection->active = false;
29 | }
30 |
31 | /**
32 | * @covers {className}::{origMethodName}
33 | * @todo Implement testEvents().
34 | */
35 | public function testEvents()
36 | {
37 | // Remove the following lines when you implement this test.
38 | $this->markTestIncomplete(
39 | 'This test has not been implemented yet.'
40 | );
41 | }
42 |
43 | /**
44 | * @covers {className}::{origMethodName}
45 | * @todo Implement testAfterConstruct().
46 | */
47 | public function testAfterConstruct()
48 | {
49 | // Remove the following lines when you implement this test.
50 | $this->markTestIncomplete(
51 | 'This test has not been implemented yet.'
52 | );
53 | }
54 |
55 | /**
56 | * @covers {className}::{origMethodName}
57 | * @todo Implement testAfterFind().
58 | */
59 | public function testAfterFind()
60 | {
61 | // Remove the following lines when you implement this test.
62 | $this->markTestIncomplete(
63 | 'This test has not been implemented yet.'
64 | );
65 | }
66 |
67 | /**
68 | * @covers {className}::{origMethodName}
69 | * @todo Implement testUnsafeAttribute().
70 | */
71 | public function testUnsafeAttribute()
72 | {
73 | // Remove the following lines when you implement this test.
74 | $this->markTestIncomplete(
75 | 'This test has not been implemented yet.'
76 | );
77 | }
78 |
79 | /**
80 | * @covers {className}::{origMethodName}
81 | * @todo Implement testBeforeValidate().
82 | */
83 | public function testBeforeValidate()
84 | {
85 | // Remove the following lines when you implement this test.
86 | $this->markTestIncomplete(
87 | 'This test has not been implemented yet.'
88 | );
89 | }
90 |
91 | /**
92 | * @covers {className}::{origMethodName}
93 | * @todo Implement testAfterValidate().
94 | */
95 | public function testAfterValidate()
96 | {
97 | // Remove the following lines when you implement this test.
98 | $this->markTestIncomplete(
99 | 'This test has not been implemented yet.'
100 | );
101 | }
102 |
103 | /**
104 | * @covers {className}::{origMethodName}
105 | * @todo Implement testBeforeSave().
106 | */
107 | public function testBeforeSave()
108 | {
109 | // Remove the following lines when you implement this test.
110 | $this->markTestIncomplete(
111 | 'This test has not been implemented yet.'
112 | );
113 | }
114 |
115 | /**
116 | * @covers {className}::{origMethodName}
117 | * @todo Implement testAfterSave().
118 | */
119 | public function testAfterSave()
120 | {
121 | // Remove the following lines when you implement this test.
122 | $this->markTestIncomplete(
123 | 'This test has not been implemented yet.'
124 | );
125 | }
126 |
127 | /**
128 | * @covers {className}::{origMethodName}
129 | * @todo Implement testFindRelationByPath().
130 | */
131 | public function testFindRelationByPath()
132 | {
133 | // Remove the following lines when you implement this test.
134 | $this->markTestIncomplete(
135 | 'This test has not been implemented yet.'
136 | );
137 | }
138 |
139 | /**
140 | * @covers {className}::{origMethodName}
141 | * @todo Implement testFindPathAttribute().
142 | */
143 | public function testFindPathAttribute()
144 | {
145 | // Remove the following lines when you implement this test.
146 | $this->markTestIncomplete(
147 | 'This test has not been implemented yet.'
148 | );
149 | }
150 |
151 | /**
152 | * @covers WFormBehavior::_buildRelatedModel
153 | */
154 | public function testBuildRelatedModel()
155 | {
156 |
157 | $behavior = new WFormBehavior();
158 | $this->assertAttributeSame(null, 'relations', $behavior);
159 | $this->assertAttributeCount(0, 'relatedModels', $behavior);
160 |
161 | $method = new ReflectionMethod($behavior, '_buildRelatedModel');
162 | $method->setAccessible(true);
163 |
164 | $model = $this->getMockBuilder('CActiveRecord')
165 | ->disableOriginalConstructor()
166 | ->getMock();
167 |
168 | $model->expects($this->any())
169 | ->method('relations')
170 | ->will($this->returnValue(array(
171 | 'hasOne' => array(CActiveRecord::HAS_ONE, 'SomeModel', 'key'),
172 | 'hasMany' => array(CActiveRecord::HAS_MANY, 'SomeModel', 'key'),
173 | 'manyMany' => array(CActiveRecord::MANY_MANY, 'SomeModel', 'key'),
174 | 'belongsTo' => array(CActiveRecord::BELONGS_TO, 'SomeModel', 'key'),
175 | 'stat' => array(CActiveRecord::STAT, 'SomeModel', 'key'),
176 | )));
177 |
178 | $method->invokeArgs($behavior, array($model));
179 |
180 | $this->assertAttributeCount(4, 'relations', $behavior);
181 | $this->assertAttributeCount(4, 'relatedModels', $behavior);
182 |
183 | }
184 | }
--------------------------------------------------------------------------------
/WFormBehavior.php:
--------------------------------------------------------------------------------
1 |
4 | * @link http://weavora.com
5 | * @copyright Copyright (c) 2011 Weavora LLC
6 | */
7 |
8 | class WFormBehavior extends CActiveRecordBehavior {
9 |
10 | /**
11 | * @var array what relations we should save
12 | */
13 | public $relations = null;
14 |
15 | /**
16 | * @var array scenarios to behavior will be applied
17 | */
18 | public $scenarios = array('*');
19 |
20 | protected $relatedModels = array();
21 |
22 | protected $deleteQuery = array();
23 |
24 | // relation was specified into CActiveRecord::with();
25 | // finder and populated record are different AR instances that's why they should be static :(
26 | // could potentially cause an issue with multi threading
27 | protected static $preloadedRelations = array();
28 |
29 | /**
30 | * Extend standard AR behavior events
31 | *
32 | * @return array
33 | */
34 | public function events() {
35 | return array_merge(parent::events(), array(
36 | // @todo any ideas how to prevent using custom event for that ? Maybe create attributes dynamic for relations?
37 | 'onUnsafeAttribute' => 'unsafeAttribute',
38 | ));
39 | }
40 |
41 | /**
42 | * Initialize related models
43 | *
44 | * @param $event
45 | * @return void
46 | */
47 | public function afterConstruct($event) {
48 | $this->_buildRelatedModel($event->sender);
49 | }
50 |
51 | /**
52 | * Cache preloaded relation by CActiveRecord::with() method
53 | *
54 | * @param $event
55 | * @return void
56 | */
57 | public function beforeFind($event) {
58 | $model = $event->sender;
59 | self::$preloadedRelations = array();
60 | foreach((array) $model->getDbCriteria()->with as $key => $value) {
61 | self::$preloadedRelations[] = is_numeric($key) ? $value : $key;
62 | }
63 | }
64 |
65 | /**
66 | * Rebuild related models
67 | *
68 | * @param $event
69 | * @return void
70 | */
71 | public function afterFind($event) {
72 | $this->_buildRelatedModel($event->sender);
73 | }
74 |
75 | /**
76 | * Set related models attributes
77 | *
78 | * @param $event
79 | * @return void
80 | */
81 | public function unsafeAttribute($event) {
82 | $relation = $event->params['name'];
83 | if (isset($this->relatedModels[$relation])) {
84 | $this->relatedModels[$relation]->setAttributes($event->params['value']);
85 | }
86 |
87 | }
88 |
89 | /**
90 | * Handle file inputs
91 | *
92 | * @param $event
93 | */
94 | public function beforeValidate($event) {
95 | $model = $event->sender;
96 |
97 | // create CUploadedFile for all file inputs
98 | $files = new WFileIterator($model);
99 | foreach($files as $path => $file) {
100 | $relation = $this->findRelationByPath($model, $path);
101 | $attribute = $this->findPathAttribute($path);
102 | if ($relation) {
103 | $relation->setAttribute($attribute, $file);
104 | }
105 | }
106 | }
107 |
108 | /**
109 | * Validate related models
110 | *
111 | * @param $event
112 | * @return void
113 | */
114 | public function afterValidate($event) {
115 | $model = $event->sender;
116 | foreach ($this->relatedModels as $relatedModel) {
117 | if (!$relatedModel->validate()) {
118 | $model->addError($relatedModel->name, $relatedModel->name . ' is not valid');
119 | }
120 | }
121 | }
122 |
123 | /**
124 | * Save related models that affect parent models
125 | *
126 | * @param $event
127 | * @return void
128 | */
129 | public function beforeSave($event) {
130 | foreach ($this->relatedModels as $relatedModel) {
131 | if (in_array($relatedModel->type, array(CActiveRecord::BELONGS_TO))) {
132 | if (!$relatedModel->save())
133 | $event->isValid = false;
134 |
135 | $this->deleteQuery[] = $relatedModel;
136 | }
137 | }
138 | }
139 |
140 | /**
141 | * Save related models that depend on the parent model
142 | *
143 | * @param $event
144 | * @return void
145 | */
146 | public function afterSave($event) {
147 | foreach ($this->relatedModels as $relatedModel) {
148 | if (in_array($relatedModel->type, array(CActiveRecord::HAS_ONE, CActiveRecord::HAS_MANY, CActiveRecord::MANY_MANY))) {
149 | $relatedModel->save();
150 |
151 | $this->deleteQuery[] = $relatedModel;
152 | }
153 | }
154 |
155 | foreach($this->deleteQuery as $relation) {
156 | $relation->lazyDelete();
157 | }
158 | }
159 |
160 | /**
161 | * Delete related models that depend on the parent model
162 | *
163 | * @param $event
164 | * @return void
165 | */
166 | public function afterDelete($event) {
167 | foreach ($this->relatedModels as $relatedModel) {
168 | if (in_array($relatedModel->type, array(CActiveRecord::HAS_ONE, CActiveRecord::HAS_MANY, CActiveRecord::MANY_MANY))) {
169 | $relatedModel->delete();
170 | }
171 | }
172 | }
173 |
174 | /**
175 | * Find related model by path (e.g. categories.0.name)
176 | *
177 | * @param $parentModel parent model
178 | * @param $path path
179 | * @return CActiveRecord
180 | */
181 | public function findRelationByPath($parentModel, $path) {
182 | $model = $parentModel;
183 | $pathPortions = explode(".", trim($path, "."));
184 | if (count($pathPortions)) {
185 | $attribute = array_pop($pathPortions);
186 | }
187 |
188 | foreach($pathPortions as $portion) {
189 | if (empty($model[$portion]))
190 | return null;
191 | $model = $model[$portion];
192 | }
193 | return $model;
194 | }
195 |
196 | /**
197 | * Find an attribute name in the path (e.g. categories.0.name)
198 | *
199 | * @param $path path
200 | * @return string attribute name
201 | */
202 | public function findPathAttribute($path) {
203 | $pathPortions = explode(".", trim($path, "."));
204 | return count($pathPortions) ? end($pathPortions) : null;
205 | }
206 |
207 | /**
208 | * Rebuild related models
209 | *
210 | * @param $parentModel
211 | * @return void
212 | */
213 | protected function _buildRelatedModel($parentModel) {
214 | $this->relatedModels = array();
215 | if (is_null($this->relations)) {
216 | $this->relations = array_keys($parentModel->relations());
217 | }
218 | foreach ($this->relations as $index => $options) {
219 | $relation = $index;
220 |
221 | if (is_numeric($index)) {
222 | $relation = $options;
223 | $options = array();
224 | }
225 |
226 | if (($relationModel = WFormRelation::getInstance($parentModel, $relation, $options)) !== null) {
227 | $this->relatedModels[$relation] = $relationModel;
228 | $this->relatedModels[$relation]->setPreloaded(in_array($relation, self::$preloadedRelations));
229 | } else {
230 | unset($this->relations[$index]);
231 | }
232 |
233 | }
234 | }
235 | }
236 |
--------------------------------------------------------------------------------
/tests/unit/WFormTest.php:
--------------------------------------------------------------------------------
1 | object = new WForm;
21 | }
22 |
23 | /**
24 | * Tears down the fixture, for example, closes a network connection.
25 | * This method is called after a test is executed.
26 | */
27 | protected function tearDown()
28 | {
29 | }
30 |
31 | /**
32 | * @covers {className}::{origMethodName}
33 | * @todo Implement testLabel().
34 | */
35 | public function testLabel()
36 | {
37 | // Remove the following lines when you implement this test.
38 | $this->markTestIncomplete(
39 | 'This test has not been implemented yet.'
40 | );
41 | }
42 |
43 | /**
44 | * @covers {className}::{origMethodName}
45 | * @todo Implement testLabelEx().
46 | */
47 | public function testLabelEx()
48 | {
49 | // Remove the following lines when you implement this test.
50 | $this->markTestIncomplete(
51 | 'This test has not been implemented yet.'
52 | );
53 | }
54 |
55 | /**
56 | * @covers {className}::{origMethodName}
57 | * @todo Implement testTextField().
58 | */
59 | public function testTextField()
60 | {
61 | // Remove the following lines when you implement this test.
62 | $this->markTestIncomplete(
63 | 'This test has not been implemented yet.'
64 | );
65 | }
66 |
67 | /**
68 | * @covers {className}::{origMethodName}
69 | * @todo Implement testHiddenField().
70 | */
71 | public function testHiddenField()
72 | {
73 | // Remove the following lines when you implement this test.
74 | $this->markTestIncomplete(
75 | 'This test has not been implemented yet.'
76 | );
77 | }
78 |
79 | /**
80 | * @covers {className}::{origMethodName}
81 | * @todo Implement testPasswordField().
82 | */
83 | public function testPasswordField()
84 | {
85 | // Remove the following lines when you implement this test.
86 | $this->markTestIncomplete(
87 | 'This test has not been implemented yet.'
88 | );
89 | }
90 |
91 | /**
92 | * @covers {className}::{origMethodName}
93 | * @todo Implement testTextArea().
94 | */
95 | public function testTextArea()
96 | {
97 | // Remove the following lines when you implement this test.
98 | $this->markTestIncomplete(
99 | 'This test has not been implemented yet.'
100 | );
101 | }
102 |
103 | /**
104 | * @covers {className}::{origMethodName}
105 | * @todo Implement testFileField().
106 | */
107 | public function testFileField()
108 | {
109 | // Remove the following lines when you implement this test.
110 | $this->markTestIncomplete(
111 | 'This test has not been implemented yet.'
112 | );
113 | }
114 |
115 | /**
116 | * @covers {className}::{origMethodName}
117 | * @todo Implement testRadioButton().
118 | */
119 | public function testRadioButton()
120 | {
121 | // Remove the following lines when you implement this test.
122 | $this->markTestIncomplete(
123 | 'This test has not been implemented yet.'
124 | );
125 | }
126 |
127 | /**
128 | * @covers {className}::{origMethodName}
129 | * @todo Implement testCheckBox().
130 | */
131 | public function testCheckBox()
132 | {
133 | // Remove the following lines when you implement this test.
134 | $this->markTestIncomplete(
135 | 'This test has not been implemented yet.'
136 | );
137 | }
138 |
139 | /**
140 | * @covers {className}::{origMethodName}
141 | * @todo Implement testDropDownList().
142 | */
143 | public function testDropDownList()
144 | {
145 | // Remove the following lines when you implement this test.
146 | $this->markTestIncomplete(
147 | 'This test has not been implemented yet.'
148 | );
149 | }
150 |
151 | /**
152 | * @covers {className}::{origMethodName}
153 | * @todo Implement testListBox().
154 | */
155 | public function testListBox()
156 | {
157 | // Remove the following lines when you implement this test.
158 | $this->markTestIncomplete(
159 | 'This test has not been implemented yet.'
160 | );
161 | }
162 |
163 | /**
164 | * @covers {className}::{origMethodName}
165 | * @todo Implement testCheckBoxList().
166 | */
167 | public function testCheckBoxList()
168 | {
169 | // Remove the following lines when you implement this test.
170 | $this->markTestIncomplete(
171 | 'This test has not been implemented yet.'
172 | );
173 | }
174 |
175 | /**
176 | * @covers {className}::{origMethodName}
177 | * @todo Implement testRadioButtonList().
178 | */
179 | public function testRadioButtonList()
180 | {
181 | // Remove the following lines when you implement this test.
182 | $this->markTestIncomplete(
183 | 'This test has not been implemented yet.'
184 | );
185 | }
186 |
187 | /**
188 | * @covers {className}::{origMethodName}
189 | * @todo Implement testError().
190 | */
191 | public function testError()
192 | {
193 | // Remove the following lines when you implement this test.
194 | $this->markTestIncomplete(
195 | 'This test has not been implemented yet.'
196 | );
197 | }
198 |
199 | /**
200 | * @covers {className}::{origMethodName}
201 | * @todo Implement testResolveModel().
202 | */
203 | public function testResolveModel()
204 | {
205 | // Remove the following lines when you implement this test.
206 | $this->markTestIncomplete(
207 | 'This test has not been implemented yet.'
208 | );
209 | }
210 |
211 | /**
212 | * @covers {className}::{origMethodName}
213 | * @todo Implement testResolveName().
214 | */
215 | public function testResolveName()
216 | {
217 | // Remove the following lines when you implement this test.
218 | $this->markTestIncomplete(
219 | 'This test has not been implemented yet.'
220 | );
221 | }
222 |
223 | /**
224 | * @covers {className}::{origMethodName}
225 | * @todo Implement testResolveAttribute().
226 | */
227 | public function testResolveAttribute()
228 | {
229 | // Remove the following lines when you implement this test.
230 | $this->markTestIncomplete(
231 | 'This test has not been implemented yet.'
232 | );
233 | }
234 |
235 | /**
236 | * @covers {className}::{origMethodName}
237 | * @todo Implement testResolveArgs().
238 | */
239 | public function testResolveArgs()
240 | {
241 | // Remove the following lines when you implement this test.
242 | $this->markTestIncomplete(
243 | 'This test has not been implemented yet.'
244 | );
245 | }
246 |
247 | /**
248 | * @covers {className}::{origMethodName}
249 | * @todo Implement testResolveLabel().
250 | */
251 | public function testResolveLabel()
252 | {
253 | // Remove the following lines when you implement this test.
254 | $this->markTestIncomplete(
255 | 'This test has not been implemented yet.'
256 | );
257 | }
258 | }
259 | ?>
260 |
--------------------------------------------------------------------------------
/examples/product/views/product/edit.php:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
18 | isNewRecord):?>
19 | Add Product
20 |
21 | Edit Product
22 |
23 |
24 | Back to products list
25 |
26 |
178 |
179 |
216 |
--------------------------------------------------------------------------------
/tests/unit/WFormRelationBelongsToTest.php:
--------------------------------------------------------------------------------
1 | markTestSkipped('PDO and SQLite extensions are required.');
16 |
17 | $this->_connection = new CDbConnection('sqlite::memory:');
18 | $this->_connection->active = true;
19 | $this->_connection->pdoInstance->exec(file_get_contents(dirname(__FILE__).'/../fixtures/data/sqlite.sql'));
20 | CActiveRecord::$db = $this->_connection;
21 | }
22 |
23 |
24 | protected function tearDown()
25 | {
26 | $this->_connection->active=false;
27 | }
28 |
29 | /**
30 | * @covers WFormRelationHasMany::setAttributes
31 | */
32 | public function testSetAttributes()
33 | {
34 | $product = $this->_getProductWithRelation();
35 |
36 | $product->attributes = array(
37 | 'name' => 'product_name',
38 | 'category' => array(
39 | 'name' => '12',
40 | ),
41 | );
42 |
43 | $this->assertNotEmpty($product->category);
44 | $this->assertEquals('12', $product->category->name);
45 |
46 | $product->attributes = array(
47 | 'name' => 'product_name',
48 | );
49 | $this->assertNotEmpty($product->category);
50 |
51 | $product->attributes = array(
52 | 'name' => 'product_name',
53 | 'category' => array(),
54 | );
55 | $this->assertTrue($product->category->isNewRecord);
56 |
57 | $product = $this->_getProductWithRelation(1);
58 | $this->assertNotEmpty($product->category);
59 |
60 | $product->attributes = array(
61 | 'name' => 'product_name',
62 | 'category' => array(
63 | 'id' => 1,
64 | ),
65 | );
66 |
67 | $this->assertNotEmpty($product->category);
68 | // check if exists category just updated
69 | $this->assertEquals('Auto', $product->category->name);
70 | }
71 |
72 | /**
73 | * @covers WFormRelationHasOne::save
74 | */
75 | public function testSaveIfNotSet()
76 | {
77 | $product = Product::model() ;
78 | $product->attachBehavior('wform', array(
79 | 'class' => 'WFormBehavior',
80 | 'relations' => array(
81 | 'category' => array('required' => false),
82 | ),
83 | ));
84 | $product = $product->findByPk(1);
85 | $product->attachBehavior('wform', array(
86 | 'class' => 'WFormBehavior',
87 | 'relations' => array(
88 | 'category' => array('required' => false),
89 | ),
90 | ));
91 | $product->afterFind(new CEvent($product));
92 |
93 |
94 | $this->assertEquals(true, $product->save());
95 | $this->assertNotEmpty($product->category);
96 |
97 | $product = Product::model();
98 | $product->attachBehavior('wform', array(
99 | 'class' => 'WFormBehavior',
100 | 'relations' => array(
101 | 'category' => array('required' => false),
102 | ),
103 | ));
104 | $product = $product->with('category')->findByPk(1);
105 | $product->attachBehavior('wform', array(
106 | 'class' => 'WFormBehavior',
107 | 'relations' => array(
108 | 'category' => array('required' => false),
109 | ),
110 | ));
111 | $product->afterFind(new CEvent($product));
112 |
113 | $this->assertEquals(true, $product->save());
114 |
115 | $this->assertNotEmpty($product->category);
116 | }
117 |
118 | /**
119 | * @covers WFormRelationHasMany::validate
120 | * @dataProvider validateProvider
121 | */
122 | public function testValidate($expectedResult, $relationOptions, $relationAttribute, $onFailComment = "")
123 | {
124 | $product = $this->_getProductWithRelation(null, $relationOptions);
125 |
126 | $product->attributes = array(
127 | 'name' => 'product_name',
128 | 'category' => $relationAttribute,
129 | );
130 |
131 | $this->assertEquals($expectedResult, $product->validate(), $onFailComment);
132 | }
133 |
134 | public function validateProvider()
135 | {
136 | return array(
137 | // required=true
138 | array(
139 | 'result' => true,
140 | 'relationOptions' => array('required' => true),
141 | 'relationAttribute' => array(
142 | 'name' => '12x12'
143 | ),
144 | 'comment' => 'required, 1 valid related object'
145 | ),
146 | array(
147 | 'result' => false,
148 | 'relationOptions' => array('required' => true),
149 | 'relationAttribute' => array(
150 | 'name' => ''
151 | ),
152 | 'comment' => 'required, 1 invalid related object'
153 | ),
154 | array(
155 | 'result' => false,
156 | 'relationOptions' => array('required' => true),
157 | 'relationAttribute' => array(),
158 | 'comment' => 'required, 0 related objects'
159 | ),
160 | // required=false
161 | array(
162 | 'result' => true,
163 | 'relationOptions' => array('required' => false),
164 | 'relationAttribute' => array(
165 | 'name' => '12x12'
166 | ),
167 | 'comment' => 'not required, 1 valid related object'
168 | ),
169 | array(
170 | 'result' => false,
171 | 'relationOptions' => array('required' => false),
172 | 'relationAttribute' => array(
173 | 'name' => ''
174 | ),
175 | 'comment' => 'not required, 1 invalid related objects'
176 | ),
177 | array(
178 | 'result' => true,
179 | 'relationOptions' => array('required' => false),
180 | 'relationAttribute' => null,
181 | 'comment' => 'not required, 0 related objects'
182 | ),
183 | );
184 | }
185 |
186 | /**
187 | * @covers WFormRelationHasMany::save
188 | * @dataProvider saveProvider
189 | */
190 | public function testSave($expectedResult, $relationOptions, $relationAttribute, $onFailComment = "")
191 | {
192 | $product = $this->_getProductWithRelation(null, $relationOptions);
193 |
194 | $product->attributes = array(
195 | 'name' => 'product_name',
196 | 'category' => $relationAttribute,
197 | );
198 |
199 | $this->assertEquals($expectedResult['saved'], $product->save(), $onFailComment);
200 | if ($expectedResult['relationsCount'] > 0) {
201 | $this->assertNotEmpty($product->category, $onFailComment);
202 | } else {
203 | $this->assertEmpty($product->category, $onFailComment);
204 | }
205 |
206 | }
207 |
208 | public function saveProvider()
209 | {
210 | return array(
211 | // required=true
212 | array(
213 | 'result' => array('saved' => true, 'relationsCount' => 1),
214 | 'relationOptions' => array('required' => true),
215 | 'relationAttribute' => array(
216 | 'name' => 'tag_name'
217 | ),
218 | 'comment' => 'required, 1 valid related object'
219 | ),
220 | array(
221 | 'result' => array('saved' => false, 'relationsCount' => 1),
222 | 'relationOptions' => array('required' => true),
223 | 'relationAttribute' => array(
224 | 'name' => ''
225 | ),
226 | 'comment' => 'required, 1 invalid related object'
227 | ),
228 | array(
229 | 'result' => array('saved' => false, 'relationsCount' => 1),
230 | 'relationOptions' => array('required' => true),
231 | 'relationAttribute' => null,
232 | 'comment' => 'required, 0 related objects'
233 | ),
234 | // required=false
235 | array(
236 | 'result' => array('saved' => true, 'relationsCount' => 1),
237 | 'relationOptions' => array('required' => false),
238 | 'relationAttribute' => array(
239 | 'name' => 'tag_name'
240 | ),
241 | 'comment' => 'not required, 1 valid related object'
242 | ),
243 | array(
244 | 'result' => array('saved' => false, 'relationsCount' => 1),
245 | 'relationOptions' => array('required' => false),
246 | 'relationAttribute' => array(
247 | 'name' => ''
248 | ),
249 | 'comment' => 'not required, 1 invalid related objects'
250 | ),
251 | array(
252 | 'result' => array('saved' => true, 'relationsCount' => 0),
253 | 'relationOptions' => array('required' => false),
254 | 'relationAttribute' => null,
255 | 'comment' => 'not required, 0 related objects'
256 | ),
257 | );
258 | }
259 |
260 | /**
261 | * @covers WFormRelationHasMany::getRelatedModel
262 | */
263 | public function testGetRelatedModel()
264 | {
265 | $product = $this->_getProductWithRelation();
266 | $relation = WFormRelation::getInstance($product, 'category');
267 |
268 | $this->assertEmpty($relation->getRelatedModel(false));
269 | // $this->assertNotEmpty($relation->getRelatedModel(true));
270 |
271 | $product->attributes = array(
272 | 'name' => 'product_name',
273 | 'category' => array(
274 | 'id' => 1,
275 | 'name' => '10'
276 | ),
277 | );
278 |
279 | $this->assertNotEmpty($relation->getRelatedModel());
280 |
281 | $product = $this->_getProductWithRelation(1);
282 | $relation = WFormRelation::getInstance($product, 'category');
283 |
284 | $this->assertNotEmpty($relation->getRelatedModel());
285 | }
286 |
287 | /**
288 | * @param null $id
289 | * @param array $relationOptions
290 | * @return Product
291 | */
292 | protected function _getProductWithRelation($id = null, $relationOptions = array())
293 | {
294 | $product = $id ? Product::model()->findByPk($id) : new Product();
295 | $product->attachBehavior('wform', array(
296 | 'class' => 'WFormBehavior',
297 | 'relations' => array(
298 | 'category' => $relationOptions,
299 | ),
300 | ));
301 | $product->afterConstruct(new CEvent($product));
302 |
303 | return $product;
304 | }
305 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Yii Composite Form Extension
2 | ==========================
3 |
4 | The extension that can greatly simplify processing of complex forms with multiple relations.
5 |
6 | [Weavora's](http://weavora.com) Git Repo - [https://github.com/weavora/wform](https://github.com/weavora/wform)
7 |
8 | **Features**:
9 |
10 | * Easy composite form processing
11 | * Fast configuration
12 | * Support of all standard relations: has_one, belongs_to, has_many and many_many
13 |
14 | Configuration
15 | -----
16 |
17 | 1) Download and unpack the source into the protected/extensions/ folder.
18 |
19 | 2) Below you can see the config settings for import:
20 |
21 | ```php
22 | array(
27 | ...
28 | 'ext.wform.*',
29 | ),
30 | ...
31 | );
32 | ```
33 |
34 | 3) The extension requires changing CActiveRecord::onUnsafeAttribute. Here are a few options for that:
35 |
36 | a) Extend all your models/forms from WActiveRecord instead of CActiveRecord
37 |
38 | b) If you have already modified the class for active record, extend it from WActiveRecord or add the onUnsafeAttribute method:
39 |
40 | ```php
41 | $name, 'value' => $value));
65 | $this->raiseEvent('onUnsafeAttribute', $event);
66 | return parent::onUnsafeAttribute($name, $value);
67 | }
68 | }
69 | ```
70 |
71 | Usage
72 | -----
73 |
74 | 1) Modify the model: define relations and attach behavior.
75 | You can also create a separate class for the form extended from your model.
76 |
77 | ```php
78 | array(self::HAS_ONE, 'HasOneModel', 'my_model_fk_into_related_model'),
85 | 'belongsToRelation' => array(self::BELONGS_TO, 'BelongsToModel', 'related_model_fk_into_my_model'),
86 | 'hasManyRelation' => array(self::HAS_MANY, 'HasManyModel', 'my_model_fk_into_related_model'),
87 | 'manyManyRelation' => array(self::MANY_MANY, 'ManyManyModel', 'linker(my_model_id,related_model_id)'),
88 | );
89 | }
90 | ...
91 | public function behaviors() {
92 | return array(
93 | // attach wform behavior
94 | 'wform' => array(
95 | 'class' => 'ext.wform.WFormBehavior',
96 | // define relations which would be processed
97 | 'relations' => array('hasOneRelation', 'belongsToRelation', 'hasManyRelation', 'manyManyRelation'),
98 | ),
99 | // or you could allow to skip some relation saving if it was submitted empty
100 | 'wform' => array(
101 | 'class' => 'ext.wform.WFormBehavior',
102 | 'relations' => array(
103 | 'hasOneRelation' => array(
104 | 'required' => true, // declare that a relation item should be valid (default for HAS_ONE: false)
105 | 'cascadeDelete' => true, // declare if a relation item would be deleted during parent model deletion (default for HAS_ONE: true)
106 | ),
107 | 'belongsToRelation' => array(
108 | 'required' => true, // declare all relation items to be valid (default for BELONGS_TO: false)
109 | ),
110 | 'hasManyRelation' => array(
111 | 'required' => true, // declare all relation items to be valid (default for HAS_MANY: false)
112 | 'unsetInvalid' => true, // will unset invalid relation items during save or validate actions (default for HAS_MANY: false)
113 | 'cascadeDelete' => true, // declare if relation items would be deleted during parent model deletion (default for HAS_MANY: true)
114 | ),
115 | 'manyManyRelation' => array(
116 | 'required' => true, // declare all relation items to be valid (default for MANY_MANY: false)
117 | 'unsetInvalid' => true, // will unset invalid relation items during save or validate actions (default for MANY_MANY: false)
118 | 'cascadeDelete' => true, // declare if db rows with a relation item link to model would be deleted during parent model deletion (default for MANY_MANY: true)
119 | ),
120 | ),
121 | ),
122 | );
123 | }
124 | ...
125 | }
126 | ```
127 |
128 | 2) Create an action to process the form.
129 |
130 | ```php
131 | with('hasManyRelation','manyManyRelation')->findByPk($id) : new MyModel();
138 | if(Yii::app()->request->isPostRequest) {
139 | $myModel->attributes = Yii::app()->request->getPost('MyModel');
140 | if ($myModel->save()) {
141 | $this->redirect('some/page');
142 | }
143 | }
144 | $this->render('edit', array(
145 | 'model' => $myModel
146 | ));
147 | }
148 |
149 | // delete the model with relation to a single line of code :)
150 | public function actionDelete($id)
151 | {
152 | $myModel = MyModel::model()->findByPk($id);
153 | if(!empty($myModel)) {
154 | $myModel->delete();
155 | }
156 | $this->redirect('some/page');
157 | }
158 | }
159 | ```
160 |
161 | 3) Include js/jquery.multiplyforms.js jquery plugin into your layout
162 |
163 |
164 | 4) Define the form using WForm instead of CActiveForm
165 |
166 | ```php
167 | // protected/views/my/edit.php
168 |
169 | isNewRecord ? "Create" : "Update " . $model->name);?>
170 | beginWidget('WForm'); ?>
171 |
172 |
173 |
174 | labelEx($model, 'name'); ?>
175 | textField($model, 'name'); ?>
176 | error($model, 'name'); ?>
177 |
178 |
179 |
180 |
181 |
182 |
183 | labelEx($model, 'hasOneRelation.name'); ?>
184 | textField($model, 'hasOneRelation.name'); ?>
185 | error($model, 'hasOneRelation.name'); ?>
186 |
187 |
188 |
189 |
190 | labelEx($model, 'belongsToRelation.name'); ?>
191 | textField($model, 'belongsToRelation.name'); ?>
192 | error($model, 'belongsToRelation.name'); ?>
193 |
194 |
195 |
196 |
197 |
198 | hasManyRelation): ?>
199 | hasManyRelation as $index => $item): ?>
200 |
201 | isNewRecord): ?>
202 | hiddenField($model, "hasManyRelation.$index.id"); ?>
203 |
204 | labelEx($model, "hasManyRelation.$index.text"); ?>
205 | textField($model, "hasManyRelation.$index.text"); ?>
206 | error($model, "hasManyRelation.$index.text"); ?>
207 |
Delete
208 |
209 |
210 |
211 |
212 |
213 |
219 |
220 |
Add more
221 |
222 |
223 |
224 |
225 |
226 | manyManyRelation): ?>
227 | manyManyRelation as $index => $item): ?>
228 |
229 | isNewRecord): ?>
230 | hiddenField($model, "manyManyRelation.$index.id"); ?>
231 |
232 | labelEx($model, "manyManyRelation.$index.note"); ?>
233 | textField($model, "manyManyRelation.$index.note"); ?>
234 | error($model, "manyManyRelation.$index.note"); ?>
235 |
Delete
236 |
237 |
238 |
239 |
240 |
241 |
247 |
248 |
Add more
249 |
250 |
251 |
252 | isNewRecord ? 'Create' : 'Save'); ?>
253 |
254 |
255 | endWidget(); ?>
256 |
257 |
284 |
285 | ```
286 |
287 | Real Examples
288 | -----
289 |
290 | [product form example](https://github.com/weavora/wform/wiki/Example:-Product-form)
291 |
--------------------------------------------------------------------------------
/WForm.php:
--------------------------------------------------------------------------------
1 |
4 | * @link http://weavora.com
5 | * @copyright Copyright (c) 2011 Weavora LLC
6 | */
7 |
8 | class WForm extends CActiveForm
9 | {
10 | /**
11 | * Renders an HTML label for a model attribute.
12 | * @param CModel $parentModel the parent data model
13 | * @param string $attributedPath the attribute or path to related model attribute
14 | * @param array $htmlOptions additional HTML attributes.
15 | * @return string the generated label tag
16 | */
17 | public function label($parentModel, $attributedPath, $htmlOptions = array())
18 | {
19 | list($model, $attribute, $htmlOptions) = self::resolveArgs($parentModel, $attributedPath, $htmlOptions);
20 | $htmlOptions['for'] = CHtml::getIdByName($htmlOptions['name']);
21 | if (!isset($htmlOptions['label']) && ($label = self::resolveLabel($parentModel, $attributedPath)) !== null)
22 | $htmlOptions['label'] = $label;
23 | return parent::label($model, $attribute, $htmlOptions);
24 | }
25 |
26 | /**
27 | * Renders an HTML label for a model attribute.
28 | * @param CModel $parentModel the parent data model
29 | * @param string $attributedPath the attribute or path to related model attribute
30 | * @param array $htmlOptions additional HTML attributes.
31 | * @return string the generated label tag
32 | */
33 | public function labelEx($parentModel, $attributedPath, $htmlOptions = array())
34 | {
35 | list($model, $attribute, $htmlOptions) = self::resolveArgs($parentModel, $attributedPath, $htmlOptions);
36 | $htmlOptions['for'] = CHtml::getIdByName($htmlOptions['name']);
37 | if (!isset($htmlOptions['label']) && ($label = self::resolveLabel($parentModel, $attributedPath)) !== null)
38 | $htmlOptions['label'] = $label;
39 | return parent::labelEx($model, $attribute, $htmlOptions);
40 | }
41 |
42 | /**
43 | * Renders a text field for a model attribute.
44 | * @param CModel $parentModel the parent data model
45 | * @param string $attributedPath the attribute or path to related model attribute
46 | * @param array $htmlOptions additional HTML attributes.
47 | * @return string the generated input field
48 | */
49 | public function textField($parentModel, $attributedPath, $htmlOptions = array())
50 | {
51 | list($model, $attribute, $htmlOptions) = self::resolveArgs($parentModel, $attributedPath, $htmlOptions);
52 | return parent::textField($model, $attribute, $htmlOptions);
53 | }
54 |
55 | /**
56 | * Renders a hidden field for a model attribute.
57 | * @param CModel $parentModel the parent data model
58 | * @param string $attributedPath the attribute or path to related model attribute
59 | * @param array $htmlOptions additional HTML attributes.
60 | * @return string the generated input field
61 | */
62 | public function hiddenField($parentModel, $attributedPath, $htmlOptions = array())
63 | {
64 | list($model, $attribute, $htmlOptions) = self::resolveArgs($parentModel, $attributedPath, $htmlOptions);
65 | return parent::hiddenField($model, $attribute, $htmlOptions);
66 | }
67 |
68 | /**
69 | * Renders a password field for a model attribute.
70 | * @param CModel $parentModel the parent data model
71 | * @param string $attributedPath the attribute or path to related model attribute
72 | * @param array $htmlOptions additional HTML attributes.
73 | * @return string the generated input field
74 | */
75 | public function passwordField($parentModel, $attributedPath, $htmlOptions = array())
76 | {
77 | list($model, $attribute, $htmlOptions) = self::resolveArgs($parentModel, $attributedPath, $htmlOptions);
78 | return parent::passwordField($model, $attribute, $htmlOptions);
79 | }
80 |
81 | /**
82 | * Renders a text area for a model attribute.
83 | * @param CModel $parentModel the parent data model
84 | * @param string $attributedPath the attribute or path to related model attribute
85 | * @param array $htmlOptions additional HTML attributes.
86 | * @return string the generated text area
87 | */
88 | public function textArea($parentModel, $attributedPath, $htmlOptions = array())
89 | {
90 | list($model, $attribute, $htmlOptions) = self::resolveArgs($parentModel, $attributedPath, $htmlOptions);
91 | return parent::textArea($model, $attribute, $htmlOptions);
92 | }
93 |
94 | /**
95 | * Renders a file field for a model attribute.
96 | * @param CModel $parentModel the parent data model
97 | * @param string $attributedPath the attribute or path to related model attribute
98 | * @param array $htmlOptions additional HTML attributes
99 | * @return string the generated input field
100 | */
101 | public function fileField($parentModel, $attributedPath, $htmlOptions = array())
102 | {
103 | list($model, $attribute, $htmlOptions) = self::resolveArgs($parentModel, $attributedPath, $htmlOptions);
104 | return parent::fileField($model, $attribute, $htmlOptions);
105 | }
106 |
107 | /**
108 | * Renders a radio button for a model attribute.
109 | * @param CModel $parentModel the parent data model
110 | * @param string $attributedPath the attribute or path to related model attribute
111 | * @param array $htmlOptions additional HTML attributes.
112 | * @return string the generated radio button
113 | */
114 | public function radioButton($parentModel, $attributedPath, $htmlOptions = array())
115 | {
116 | list($model, $attribute, $htmlOptions) = self::resolveArgs($parentModel, $attributedPath, $htmlOptions);
117 | return parent::radioButton($model, $attribute, $htmlOptions);
118 | }
119 |
120 | /**
121 | * Renders a checkbox for a model attribute.
122 | * @param CModel $parentModel the parent data model
123 | * @param string $attributedPath the attribute or path to related model attribute
124 | * @param array $htmlOptions additional HTML attributes.
125 | * @return string the generated check box
126 | */
127 | public function checkBox($parentModel, $attributedPath, $htmlOptions = array())
128 | {
129 | list($model, $attribute, $htmlOptions) = self::resolveArgs($parentModel, $attributedPath, $htmlOptions);
130 | return parent::checkBox($model, $attribute, $htmlOptions);
131 | }
132 |
133 | /**
134 | * Renders a dropdown list for a model attribute.
135 | * @param CModel $parentModel the parent data model
136 | * @param string $attributedPath the attribute or path to related model attribute
137 | * @param array $data data for generating the list options (value=>display)
138 | * @param array $htmlOptions additional HTML attributes.
139 | * @return string the generated drop down list
140 | */
141 | public function dropDownList($parentModel, $attributedPath, $data, $htmlOptions = array())
142 | {
143 | list($model, $attribute, $htmlOptions) = self::resolveArgs($parentModel, $attributedPath, $htmlOptions);
144 | return parent::dropDownList($model, $attribute, $data, $htmlOptions);
145 | }
146 |
147 | /**
148 | * Renders a list box for a model attribute.
149 | * @param CModel $parentModel the parent data model
150 | * @param string $attributedPath the attribute or path to related model attribute
151 | * @param array $data data for generating the list options (value=>display)
152 | * @param array $htmlOptions additional HTML attributes.
153 | * @return string the generated list box
154 | */
155 | public function listBox($parentModel, $attributedPath, $data, $htmlOptions = array())
156 | {
157 | list($model, $attribute, $htmlOptions) = self::resolveArgs($parentModel, $attributedPath, $htmlOptions);
158 | return parent::listBox($model, $attribute, $data, $htmlOptions);
159 | }
160 |
161 | /**
162 | * Renders a checkbox list for a model attribute.
163 | * @param CModel $parentModel the parent data model
164 | * @param string $attributedPath the attribute or path to related model attribute
165 | * @param array $data value-label pairs used to generate the check box list.
166 | * @param array $htmlOptions additional HTML options.
167 | * @return string the generated check box list
168 | */
169 | public function checkBoxList($parentModel, $attributedPath, $data, $htmlOptions = array())
170 | {
171 | list($model, $attribute, $htmlOptions) = self::resolveArgs($parentModel, $attributedPath, $htmlOptions);
172 | return parent::checkBoxList($model, $attribute, $data, $htmlOptions);
173 | }
174 |
175 | /**
176 | * Renders a radio button list for a model attribute.
177 | * @param CModel $parentModel the parent data model
178 | * @param string $attributedPath the attribute or path to related model attribute
179 | * @param array $data value-label pairs used to generate the radio button list.
180 | * @param array $htmlOptions additional HTML options.
181 | * @return string the generated radio button list
182 | */
183 | public function radioButtonList($parentModel, $attributedPath, $data, $htmlOptions = array())
184 | {
185 | list($model, $attribute, $htmlOptions) = self::resolveArgs($parentModel, $attributedPath, $htmlOptions);
186 | return parent::radioButtonList($model, $attribute, $data, $htmlOptions);
187 | }
188 |
189 | /**
190 | * Displays the first validation error for a model attribute.
191 | * This is similar to {@link CHtml::error} except that it registers the model attribute
192 | * so that if its value is changed by users, an AJAX validation may be triggered.
193 | * @param CModel $parentModel the parent data model
194 | * @param string $attributedPath the attribute name
195 | * @param array $htmlOptions additional HTML attributes to be rendered in the container div tag.
196 | * @param boolean $enableAjaxValidation whether to enable AJAX validation for the specified attribute.
197 | * @param boolean $enableClientValidation whether to enable client-side validation for the specified attribute.
198 | * @return string the validation result (error display or success message).
199 | * @see CHtml::error
200 | */
201 | public function error($parentModel, $attributedPath, $htmlOptions=array(), $enableAjaxValidation=true, $enableClientValidation=true)
202 | {
203 | list($model, $attribute, $htmlOptions) = self::resolveArgs($parentModel, $attributedPath, $htmlOptions);
204 | return parent::error($model, $attribute, $htmlOptions, $enableAjaxValidation, $enableClientValidation);
205 | }
206 |
207 | public static function resolveModel($parentModel, $attributedPath)
208 | {
209 | $model = $parentModel;
210 | $pathPortions = explode('.', $attributedPath);
211 |
212 | // last portion is always model attribute
213 | $attribute = array_pop($pathPortions);
214 | foreach ($pathPortions as $index => $portion) {
215 | // handle 'parent.statuses..id'
216 | if ($portion == '') {
217 | // portion becomes to new index
218 | $portion = is_array($model) ? count($model) : 0;
219 | }
220 |
221 | // handle 'parent.1.'
222 | if (is_numeric($portion)) {
223 | if (!empty($model) && !is_array($model)) {
224 | throw new Exception("Incorrect '..' or '.<index>.' usage");
225 | }
226 |
227 | $nextModel = isset($model[$portion]) ? $model[$portion]
228 | : self::createRelationModel($parentModel, $pathPortions[$index - 1]);
229 | }
230 | // handle 'parent.status' when status relation is empty (new model required)
231 | elseif (empty($model->{$portion})) {
232 |
233 | $nextModel = self::createRelationModel($model, $portion, true);
234 | }
235 | // handle 'parent.status'
236 | else {
237 | $nextModel = $model->{$portion};
238 | }
239 |
240 | // shift models
241 | $parentModel = $model;
242 | $model = $nextModel;
243 | }
244 |
245 | return $model;
246 | }
247 |
248 | public static function resolveName($parentModel, $attributedPath)
249 | {
250 | $name = get_class($parentModel);
251 | $pathPortions = explode('.', $attributedPath);
252 | foreach ($pathPortions as $key => $pathPortion) {
253 | if ($pathPortion === '')
254 | $pathPortion = '{index}';
255 | $name .= '[' . $pathPortion . ']';
256 | }
257 | return $name;
258 | }
259 |
260 | public static function resolveAttribute($attributedPath)
261 | {
262 | $pathPortions = explode('.', $attributedPath);
263 | return trim(end($pathPortions));
264 | }
265 |
266 | public static function resolveArgs($parentModel, $attributedPath, $htmlOptions = array())
267 | {
268 | $model = self::resolveModel($parentModel, $attributedPath);
269 | $attribute = self::resolveAttribute($attributedPath);
270 | if (empty($htmlOptions['name']))
271 | $htmlOptions['name'] = self::resolveName($parentModel, $attributedPath);
272 |
273 | return array($model, $attribute, $htmlOptions);
274 | }
275 |
276 | public static function resolveLabel($parentModel, $attributedPath)
277 | {
278 | $attribute = str_replace('..','.', $attributedPath);
279 | return $parentModel->getAttributeLabel($attribute);
280 | }
281 |
282 | protected static function createRelationModel($model, $relation, $allowMany = false)
283 | {
284 | $relations = $model->relations();
285 | if (!array_key_exists($relation, $relations))
286 | throw new Exception("Undefined relation " . $relation);
287 |
288 | $relationType = $relations[$relation][0];
289 | $relationModelClass = $relations[$relation][1];
290 |
291 | if ($allowMany && in_array($relationType, array(CActiveRecord::HAS_MANY, CActiveRecord::MANY_MANY))) {
292 | $model = array();
293 | } else {
294 | $model = new $relationModelClass();
295 | }
296 |
297 | return $model;
298 | }
299 | }
300 |
--------------------------------------------------------------------------------
/tests/unit/WFormRelationHasOneTest.php:
--------------------------------------------------------------------------------
1 | markTestSkipped('PDO and SQLite extensions are required.');
16 |
17 | $this->_connection = new CDbConnection('sqlite::memory:');
18 | $this->_connection->active = true;
19 | $this->_connection->pdoInstance->exec(file_get_contents(dirname(__FILE__).'/../fixtures/data/sqlite.sql'));
20 | CActiveRecord::$db = $this->_connection;
21 | }
22 |
23 |
24 | protected function tearDown()
25 | {
26 | $this->_connection->active=false;
27 | }
28 |
29 | /**
30 | * @covers WFormRelationHasOne::setAttributes
31 | */
32 | public function testSetAttributes()
33 | {
34 | $product = $this->_getProductWithRelation();
35 |
36 | $product->attributes = array(
37 | 'name' => 'product_name',
38 | 'description' => array(
39 | 'size' => '12',
40 | ),
41 | );
42 |
43 | $this->assertNotEmpty($product->description);
44 | $this->assertEquals('12', $product->description->size);
45 |
46 | $product->attributes = array(
47 | 'name' => 'product_name',
48 | );
49 | $this->assertNotEmpty($product->description);
50 |
51 | $product->attributes = array(
52 | 'name' => 'product_name',
53 | 'description' => array(),
54 | );
55 | $this->assertTrue($product->description->isNewRecord);
56 |
57 | $product = $this->_getProductWithRelation(1);
58 | $this->assertNotEmpty($product->description);
59 |
60 | $product->attributes = array(
61 | 'name' => 'product_name',
62 | 'description' => array(
63 | 'id' => 1,
64 | ),
65 | );
66 |
67 | $this->assertNotEmpty($product->description);
68 | // check if exists description just updated
69 | $this->assertEquals('100x100', $product->description->size);
70 | }
71 |
72 | /**
73 | * @covers WFormRelationHasOne::validate
74 | * @dataProvider validateProvider
75 | */
76 | public function testValidate($expectedResult, $relationOptions, $relationAttribute, $onFailComment = "")
77 | {
78 | $product = $this->_getProductWithRelation(null, $relationOptions);
79 |
80 | $product->attributes = array(
81 | 'name' => 'product_name',
82 | 'description' => $relationAttribute,
83 | );
84 |
85 | $this->assertEquals($expectedResult, $product->validate(), $onFailComment);
86 | }
87 |
88 | public function validateProvider()
89 | {
90 | return array(
91 | // required=true
92 | array(
93 | 'result' => true,
94 | 'relationOptions' => array('required' => true),
95 | 'relationAttribute' => array(
96 | 'size' => '12x12'
97 | ),
98 | 'comment' => 'required, 1 valid related object'
99 | ),
100 | array(
101 | 'result' => false,
102 | 'relationOptions' => array('required' => true),
103 | 'relationAttribute' => array(
104 | 'size' => ''
105 | ),
106 | 'comment' => 'required, 1 invalid related object'
107 | ),
108 | array(
109 | 'result' => false,
110 | 'relationOptions' => array('required' => true),
111 | 'relationAttribute' => array(),
112 | 'comment' => 'required, 0 related objects'
113 | ),
114 | // required=false
115 | array(
116 | 'result' => true,
117 | 'relationOptions' => array('required' => false),
118 | 'relationAttribute' => array(
119 | 'size' => '12x12'
120 | ),
121 | 'comment' => 'not required, 1 valid related object'
122 | ),
123 | array(
124 | 'result' => false,
125 | 'relationOptions' => array('required' => false),
126 | 'relationAttribute' => array(
127 | 'size' => ''
128 | ),
129 | 'comment' => 'not required, 1 invalid related objects'
130 | ),
131 | array(
132 | 'result' => true,
133 | 'relationOptions' => array('required' => false),
134 | 'relationAttribute' => null,
135 | 'comment' => 'not required, 0 related objects'
136 | ),
137 | );
138 | }
139 |
140 | /**
141 | * @covers WFormRelationHasOne::save
142 | * @dataProvider saveProvider
143 | */
144 | public function testSave($expectedResult, $relationOptions, $relationAttribute, $onFailComment = "")
145 | {
146 | $product = $this->_getProductWithRelation(null, $relationOptions);
147 |
148 | $product->attributes = array(
149 | 'name' => 'product_name',
150 | 'description' => $relationAttribute,
151 | );
152 |
153 | $this->assertEquals($expectedResult['saved'], $product->save(), $onFailComment);
154 | if ($expectedResult['relationsCount'] > 0) {
155 | $this->assertNotEmpty($product->description, $onFailComment);
156 | } else {
157 | $this->assertEmpty($product->description, $onFailComment);
158 | }
159 |
160 | }
161 |
162 | public function saveProvider()
163 | {
164 | return array(
165 | // required=true
166 | array(
167 | 'result' => array('saved' => true, 'relationsCount' => 1),
168 | 'relationOptions' => array('required' => true),
169 | 'relationAttribute' => array(
170 | 'size' => 'tag_name'
171 | ),
172 | 'comment' => 'required, 1 valid related object'
173 | ),
174 | array(
175 | 'result' => array('saved' => false, 'relationsCount' => 1),
176 | 'relationOptions' => array('required' => true),
177 | 'relationAttribute' => array(
178 | 'size' => ''
179 | ),
180 | 'comment' => 'required, 1 invalid related object'
181 | ),
182 | array(
183 | 'result' => array('saved' => false, 'relationsCount' => 1),
184 | 'relationOptions' => array('required' => true),
185 | 'relationAttribute' => null,
186 | 'comment' => 'required, 0 related objects'
187 | ),
188 | // required=false
189 | array(
190 | 'result' => array('saved' => true, 'relationsCount' => 1),
191 | 'relationOptions' => array('required' => false),
192 | 'relationAttribute' => array(
193 | 'size' => 'tag_name'
194 | ),
195 | 'comment' => 'not required, 1 valid related object'
196 | ),
197 | array(
198 | 'result' => array('saved' => false, 'relationsCount' => 1),
199 | 'relationOptions' => array('required' => false),
200 | 'relationAttribute' => array(
201 | 'size' => ''
202 | ),
203 | 'comment' => 'not required, 1 invalid related objects'
204 | ),
205 | array(
206 | 'result' => array('saved' => true, 'relationsCount' => 0),
207 | 'relationOptions' => array('required' => false),
208 | 'relationAttribute' => null,
209 | 'comment' => 'not required, 0 related objects'
210 | ),
211 | );
212 | }
213 |
214 | /**
215 | * @covers WFormRelationHasOne::save
216 | */
217 | public function testSaveIfNotSet()
218 | {
219 | $product = Product::model() ;
220 | $product->attachBehavior('wform', array(
221 | 'class' => 'WFormBehavior',
222 | 'relations' => array(
223 | 'description' => array('required' => false),
224 | ),
225 | ));
226 | $product = $product->findByPk(1);
227 | $product->attachBehavior('wform', array(
228 | 'class' => 'WFormBehavior',
229 | 'relations' => array(
230 | 'description' => array('required' => false),
231 | ),
232 | ));
233 | $product->afterFind(new CEvent($product));
234 |
235 |
236 | $this->assertEquals(true, $product->save());
237 | $this->assertNotEmpty($product->description);
238 |
239 | $product = Product::model();
240 | $product->attachBehavior('wform', array(
241 | 'class' => 'WFormBehavior',
242 | 'relations' => array(
243 | 'description' => array('required' => false),
244 | ),
245 | ));
246 | $product = $product->with('description')->findByPk(1);
247 | $product->attachBehavior('wform', array(
248 | 'class' => 'WFormBehavior',
249 | 'relations' => array(
250 | 'description' => array('required' => false),
251 | ),
252 | ));
253 | $product->afterFind(new CEvent($product));
254 |
255 | $this->assertEquals(true, $product->save());
256 |
257 | $this->assertEmpty($product->description);
258 | }
259 |
260 | /**
261 | * @covers WFormRelationHasOne::getRelatedModel
262 | */
263 | public function testGetRelatedModel()
264 | {
265 | $product = $this->_getProductWithRelation();
266 | $relation = WFormRelation::getInstance($product, 'description');
267 |
268 | $this->assertEmpty($relation->getRelatedModel(false));
269 | // $this->assertNotEmpty($relation->getRelatedModel(true));
270 |
271 | $product->attributes = array(
272 | 'name' => 'product_name',
273 | 'description' => array(
274 | 'id' => 1,
275 | 'size' => '10'
276 | ),
277 | );
278 |
279 | $this->assertNotEmpty($relation->getRelatedModel());
280 |
281 | $product = $this->_getProductWithRelation(1);
282 | $relation = WFormRelation::getInstance($product, 'description');
283 |
284 | $this->assertNotEmpty($relation->getRelatedModel());
285 | }
286 |
287 | /**
288 | * @covers WFormRelationHasOne::getActualRelatedModel
289 | */
290 | public function testGetActualRelatedModel()
291 | {
292 | $product = $this->_getProductWithRelation(1, array('required' => false));
293 | $relation = WFormRelation::getInstance($product, 'description', array('required' => false));
294 |
295 | $this->assertNotEmpty($relation->getRelatedModel(false));
296 | $this->assertNotEmpty($relation->getActualRelatedModel());
297 |
298 | $product->attributes = array(
299 | 'name' => 'name',
300 | 'description' => null,
301 | );
302 |
303 |
304 | $this->assertNotEmpty($relation->getActualRelatedModel());
305 | $this->assertEmpty($relation->getRelatedModel(false));
306 |
307 | $product->attributes = array(
308 | 'name' => 'name',
309 | 'description' => array(
310 | 'size' => '10',
311 | ),
312 | );
313 |
314 | $this->assertNotEmpty($relation->getActualRelatedModel());
315 | $this->assertNotEmpty($relation->getRelatedModel(false));
316 |
317 | }
318 |
319 | /**
320 | * WFormRelationHasOne::lazyDelete
321 | * @dataProvider lazyDeleteProvider
322 | */
323 | public function testLazyDelete($expectedResult, $relationOptions, $relationAttribute, $onFailComment = "")
324 | {
325 | $product = $this->_getProductWithRelation(1, $relationOptions);
326 |
327 | $product->attributes = array(
328 | 'name' => 'name',
329 | 'description' => $relationAttribute,
330 | );
331 |
332 |
333 | $this->assertTrue(empty($product->description) == ($expectedResult['relationsCount'] == 0), $onFailComment);
334 |
335 | $product->save();
336 |
337 | $this->assertTrue(empty($product->description) == ($expectedResult['relationsCount'] == 0), $onFailComment);
338 |
339 | $refreshedDescription = $product->getRelated('description', true);
340 | $this->assertTrue(empty($refreshedDescription) == ($expectedResult['relationsCount'] == 0), $onFailComment);
341 |
342 |
343 | $relatedIds = array();
344 | $model = $product->getRelated('description', true);
345 | if ($model)
346 | $relatedIds[] = $model->primaryKey;
347 |
348 | $this->assertTrue(in_array($expectedResult['oldId'], $relatedIds) == $expectedResult['containsOld']);
349 | }
350 |
351 | public function lazyDeleteProvider()
352 | {
353 | return array(
354 | // required=true
355 | array(
356 | 'result' => array('relationsCount' => 1, 'oldId' => 1, 'containsOld' => true),
357 | 'relationOptions' => array('required' => false),
358 | 'relationAttribute' => array(
359 | 'size' => '10x10'
360 | ),
361 | 'comment' => 'new description'
362 | ),
363 | array(
364 | 'result' => array('relationsCount' => 2, 'oldId' => 1, 'containsOld' => true),
365 | 'relationOptions' => array('required' => false),
366 | 'relationAttribute' => array(
367 | 'id' => 1,
368 | 'size' => '10x10'
369 | ),
370 | 'comment' => 'old description'
371 | ),
372 | array(
373 | 'result' => array('relationsCount' => 0, 'oldId' => 1, 'containsOld' => false),
374 | 'relationOptions' => array('required' => false),
375 | 'relationAttribute' => null,
376 | 'comment' => 'empty description'
377 | ),
378 | );
379 | }
380 |
381 |
382 | /**
383 | * WFormRelationHasMany::delete
384 | */
385 | public function testDelete()
386 | {
387 | $product = $this->_getProductWithRelation(1);
388 |
389 | $this->assertNotEmpty($product->description);
390 | $id = $product->description->primaryKey;
391 | $this->assertTrue($product->delete());
392 | $this->assertEmpty(ProductDescription::model()->findByPk($id));
393 | }
394 |
395 | /**
396 | * WFormRelationHasMany::delete
397 | */
398 | public function testDeleteWithoutCascade()
399 | {
400 | $product = $this->_getProductWithRelation(1, array('cascadeDelete' => false));
401 |
402 | $this->assertNotEmpty($product->description);
403 | $id = $product->description->primaryKey;
404 | $this->assertTrue($product->delete());
405 | $this->assertNotEmpty(ProductDescription::model()->findByPk($id));
406 | }
407 |
408 | /**
409 | * @param null $id
410 | * @param array $relationOptions
411 | * @return Product
412 | */
413 | protected function _getProductWithRelation($id = null, $relationOptions = array())
414 | {
415 | $product = $id ? Product::model()->findByPk($id) : new Product();
416 | $product->attachBehavior('wform', array(
417 | 'class' => 'WFormBehavior',
418 | 'relations' => array(
419 | 'description' => $relationOptions,
420 | ),
421 | ));
422 | $product->afterConstruct(new CEvent($product));
423 |
424 | return $product;
425 | }
426 | }
--------------------------------------------------------------------------------
/tests/unit/WFormRelationManyManyTest.php:
--------------------------------------------------------------------------------
1 | markTestSkipped('PDO and SQLite extensions are required.');
12 |
13 | $this->_connection = new CDbConnection('sqlite::memory:');
14 | $this->_connection->active = true;
15 | $this->_connection->pdoInstance->exec(file_get_contents(dirname(__FILE__).'/../fixtures/data/sqlite.sql'));
16 | CActiveRecord::$db = $this->_connection;
17 | }
18 |
19 |
20 | protected function tearDown()
21 | {
22 | $this->_connection->active=false;
23 | }
24 |
25 | /**
26 | * @covers WFormRelationManyMany::setAttributes
27 | */
28 | public function testSetAttributes()
29 | {
30 | $product = $this->_getProductWithRelation();
31 |
32 | $product->attributes = array(
33 | 'name' => 'name',
34 | 'tags' => array(
35 | array(
36 | 'name' => 'tag_name',
37 | ),
38 | array(
39 | 'name' => 'tag_name2',
40 | ),
41 | ),
42 | );
43 |
44 | $this->assertCount(2, $product->tags);
45 | $this->assertEquals('tag_name', $product->tags[0]->name);
46 | $this->assertEquals('tag_name2', $product->tags[1]->name);
47 |
48 | $product->attributes = array(
49 | 'name' => 'name',
50 | );
51 | $this->assertCount(2, $product->tags);
52 |
53 | $product->attributes = array(
54 | 'name' => 'name',
55 | 'tags' => array(),
56 | );
57 | $this->assertCount(0, $product->tags);
58 |
59 | $product = $this->_getProductWithRelation(1);
60 |
61 | $product->attributes = array(
62 | 'name' => 'name',
63 | 'tags' => array(
64 | array(
65 | 'id' => 1,
66 | ),
67 | ),
68 | );
69 |
70 | $this->assertCount(1, $product->tags);
71 | // check if exists tags just updated
72 | $this->assertEquals('bad', $product->tags[0]->name);
73 | }
74 |
75 | /**
76 | * @covers WFormRelationManyMany::validate
77 | * @dataProvider validateProvider
78 | */
79 | public function testValidate($expectedResult, $relationOptions, $relationAttribute, $onFailComment = "")
80 | {
81 | $product = $this->_getProductWithRelation(null, $relationOptions);
82 |
83 | $product->attributes = array(
84 | 'name' => 'name',
85 | 'tags' => $relationAttribute,
86 | );
87 |
88 | $this->assertEquals($expectedResult, $product->validate(), $onFailComment);
89 | }
90 |
91 | /**
92 | * @covers WFormRelationManyMany::validate
93 | */
94 | public function testUnsetInvalid()
95 | {
96 | $product = $this->_getProductWithRelation(null, array('unsetInvalid' => true));
97 |
98 | $product->attributes = array(
99 | 'name' => 'name',
100 | 'tags' => array(
101 | array(
102 | 'name' => 'tag_name4'
103 | ),
104 | array(
105 | 'name' => ''
106 | ),
107 | ),
108 | );
109 |
110 | $this->assertTrue($product->validate());
111 | $this->assertCount(1, $product->tags);
112 | }
113 |
114 | public function validateProvider()
115 | {
116 | return array(
117 | // required=true
118 | array(
119 | 'result' => true,
120 | 'relationOptions' => array('required' => true),
121 | 'relationAttribute' => array(
122 | array(
123 | 'name' => 'tag_name'
124 | ),
125 | ),
126 | 'comment' => 'required, 1 valid related object'
127 | ),
128 | array(
129 | 'result' => false,
130 | 'relationOptions' => array('required' => true),
131 | 'relationAttribute' => array(
132 | array(
133 | 'name' => ''
134 | ),
135 | ),
136 | 'comment' => 'required, 1 invalid related object'
137 | ),
138 | array(
139 | 'result' => false,
140 | 'relationOptions' => array('required' => true),
141 | 'relationAttribute' => array(),
142 | 'comment' => 'required, 0 related objects'
143 | ),
144 | // required=false
145 | array(
146 | 'result' => true,
147 | 'relationOptions' => array('required' => false),
148 | 'relationAttribute' => array(
149 | array(
150 | 'name' => 'tag_name'
151 | ),
152 | ),
153 | 'comment' => 'not required, 1 valid related object'
154 | ),
155 | array(
156 | 'result' => false,
157 | 'relationOptions' => array('required' => false),
158 | 'relationAttribute' => array(
159 | array(
160 | 'name' => ''
161 | ),
162 | ),
163 | 'comment' => 'not required, 1 invalid related objects'
164 | ),
165 | array(
166 | 'result' => true,
167 | 'relationOptions' => array('required' => false),
168 | 'relationAttribute' => array(),
169 | 'comment' => 'not required, 0 related objects'
170 | ),
171 | // 'unsetInvalid' => true
172 | array(
173 | 'result' => true,
174 | 'relationOptions' => array('required' => false, 'unsetInvalid' => true),
175 | 'relationAttribute' => array(
176 | array(
177 | 'name' => ''
178 | ),
179 | ),
180 | 'comment' => 'unsetInvalid, not required, 1 invalid related objects'
181 | ),
182 | array(
183 | 'result' => true,
184 | 'relationOptions' => array('required' => true, 'unsetInvalid' => true),
185 | 'relationAttribute' => array(
186 | array(
187 | 'name' => ''
188 | ),
189 | ),
190 | 'comment' => 'unsetInvalid, required, 1 invalid related objects'
191 | ),
192 | );
193 | }
194 |
195 | /**
196 | * @covers WFormRelationManyMany::save
197 | * @dataProvider saveProvider
198 | */
199 | public function testSave($expectedResult, $relationOptions, $relationAttribute, $onFailComment = "")
200 | {
201 | $product = $this->_getProductWithRelation(null, $relationOptions);
202 |
203 | $product->attributes = array(
204 | 'name' => 'name',
205 | 'tags' => $relationAttribute,
206 | );
207 |
208 | $this->assertEquals($expectedResult['saved'], $product->save(), $onFailComment);
209 | $this->assertCount($expectedResult['relationsCount'], $product->tags, $onFailComment);
210 |
211 | $product = $this->_getProductWithRelation(1, $relationOptions);
212 |
213 | $product->attributes = array(
214 | 'name' => 'name',
215 | 'tags' => $relationAttribute,
216 | );
217 |
218 | $this->assertEquals($expectedResult['saved'], $product->save(), $onFailComment);
219 | $this->assertCount($expectedResult['relationsCount'], $product->tags, $onFailComment);
220 | }
221 |
222 | public function saveProvider()
223 | {
224 | return array(
225 | // required=true
226 | array(
227 | 'result' => array('saved' => true, 'relationsCount' => 1),
228 | 'relationOptions' => array('required' => true),
229 | 'relationAttribute' => array(
230 | array(
231 | 'name' => 'tag_name'
232 | ),
233 | ),
234 | 'comment' => 'required, 1 valid related object'
235 | ),
236 | array(
237 | 'result' => array('saved' => false, 'relationsCount' => 1),
238 | 'relationOptions' => array('required' => true),
239 | 'relationAttribute' => array(
240 | array(
241 | 'name' => ''
242 | ),
243 | ),
244 | 'comment' => 'required, 1 invalid related object'
245 | ),
246 | array(
247 | 'result' => array('saved' => false, 'relationsCount' => 0),
248 | 'relationOptions' => array('required' => true),
249 | 'relationAttribute' => array(),
250 | 'comment' => 'required, 0 related objects'
251 | ),
252 | // required=false
253 | array(
254 | 'result' => array('saved' => true, 'relationsCount' => 1),
255 | 'relationOptions' => array('required' => false),
256 | 'relationAttribute' => array(
257 | array(
258 | 'name' => 'tag_name'
259 | ),
260 | ),
261 | 'comment' => 'not required, 1 valid related object'
262 | ),
263 | array(
264 | 'result' => array('saved' => false, 'relationsCount' => 1),
265 | 'relationOptions' => array('required' => false),
266 | 'relationAttribute' => array(
267 | array(
268 | 'name' => ''
269 | ),
270 | ),
271 | 'comment' => 'not required, 1 invalid related objects'
272 | ),
273 | array(
274 | 'result' => array('saved' => true, 'relationsCount' => 0),
275 | 'relationOptions' => array('required' => false),
276 | 'relationAttribute' => array(),
277 | 'comment' => 'not required, 0 related objects'
278 | ),
279 | // 'unsetInvalid' => true
280 | array(
281 | 'result' => array('saved' => true, 'relationsCount' => 0),
282 | 'relationOptions' => array('required' => false, 'unsetInvalid' => true),
283 | 'relationAttribute' => array(
284 | array(
285 | 'name' => ''
286 | ),
287 | ),
288 | 'comment' => 'unsetInvalid, not required, 1 invalid related objects'
289 | ),
290 | array(
291 | 'result' => array('saved' => true, 'relationsCount' => 0),
292 | 'relationOptions' => array('required' => true, 'unsetInvalid' => true),
293 | 'relationAttribute' => array(
294 | array(
295 | 'name' => ''
296 | ),
297 | ),
298 | 'comment' => 'unsetInvalid, required, 1 invalid related objects'
299 | ),
300 | );
301 | }
302 |
303 | /**
304 | * @covers WFormRelationManyMany::save
305 | */
306 | public function testSaveIfNotSet()
307 | {
308 | $product = Product::model() ;
309 | $product->attachBehavior('wform', array(
310 | 'class' => 'WFormBehavior',
311 | 'relations' => array(
312 | 'tags' => array('required' => false),
313 | ),
314 | ));
315 | $product = $product->findByPk(1);
316 | $product->attachBehavior('wform', array(
317 | 'class' => 'WFormBehavior',
318 | 'relations' => array(
319 | 'tags' => array('required' => false),
320 | ),
321 | ));
322 | $product->afterFind(new CEvent($product));
323 |
324 |
325 | $this->assertEquals(true, $product->save());
326 | $this->assertCount(2, $product->tags);
327 |
328 | $product = Product::model();
329 | $product->attachBehavior('wform', array(
330 | 'class' => 'WFormBehavior',
331 | 'relations' => array(
332 | 'tags' => array('required' => false),
333 | ),
334 | ));
335 | $product = $product->with('tags')->findByPk(1);
336 | $product->attachBehavior('wform', array(
337 | 'class' => 'WFormBehavior',
338 | 'relations' => array(
339 | 'tags' => array('required' => false),
340 | ),
341 | ));
342 | $product->afterFind(new CEvent($product));
343 |
344 | $this->assertEquals(true, $product->save());
345 | $this->assertCount(0, $product->tags);
346 | }
347 |
348 | /**
349 | * @covers WFormRelationManyMany::getRelatedModels
350 | */
351 | public function testGetRelatedModels()
352 | {
353 | $product = $this->_getProductWithRelation();
354 | $relation = WFormRelation::getInstance($product, 'tags');
355 |
356 | $this->assertCount(0, $relation->getRelatedModels());
357 |
358 | $product->attributes = array(
359 | 'name' => 'name',
360 | 'tags' => array(
361 | array(
362 | 'id' => 1,
363 | 'name' => 'tag_name'
364 | ),
365 | ),
366 | );
367 |
368 | $this->assertCount(1, $relation->getRelatedModels());
369 |
370 | $product = $this->_getProductWithRelation(1);
371 | $relation = WFormRelation::getInstance($product, 'tags');
372 |
373 | $this->assertCount(2, $relation->getRelatedModels());
374 | }
375 |
376 | /**
377 | * @covers WFormRelationManyMany::getActualRelatedModels
378 | */
379 | public function testGetActualRelatedModels()
380 | {
381 | $product = $this->_getProductWithRelation(1);
382 | $relation = WFormRelation::getInstance($product, 'tags');
383 |
384 | $this->assertCount(2, $relation->getRelatedModels());
385 | $this->assertCount(2, $relation->getActualRelatedModels());
386 |
387 | $product->attributes = array(
388 | 'name' => 'name',
389 | 'tags' => array(),
390 | );
391 |
392 | $this->assertCount(2, $relation->getActualRelatedModels());
393 | $this->assertCount(0, $relation->getRelatedModels());
394 |
395 | $product->attributes = array(
396 | 'name' => 'name',
397 | 'tags' => array(
398 | array(
399 | 'name' => 'tag_name1',
400 | ),
401 | ),
402 | );
403 |
404 | $this->assertCount(2, $relation->getActualRelatedModels());
405 | $this->assertCount(1, $relation->getRelatedModels());
406 |
407 | }
408 |
409 | /**
410 | * WFormRelationManyMany::lazyDelete
411 | * @dataProvider lazyDeleteProvider
412 | */
413 | public function testLazyDelete($expectedResult, $relationOptions, $relationAttribute, $onFailComment = "")
414 | {
415 | $product = $this->_getProductWithRelation(1, $relationOptions);
416 |
417 | $product->attributes = array(
418 | 'name' => 'name',
419 | 'tags' => $relationAttribute,
420 | );
421 |
422 | $this->assertCount($expectedResult['relationsCount'], $product->tags, $onFailComment);
423 |
424 | $product->save();
425 |
426 | $this->assertCount($expectedResult['relationsCount'], $product->tags, $onFailComment);
427 | $this->assertCount($expectedResult['relationsCount'], $product->getRelated('tags', true), $onFailComment);
428 |
429 | $relatedIds = array();
430 | foreach($product->getRelated('tags', true) as $model) {
431 | $relatedIds[] = $model->primaryKey;
432 | }
433 |
434 | $this->assertTrue(in_array($expectedResult['oldId'], $relatedIds) == $expectedResult['containsOld']);
435 | }
436 |
437 | public function lazyDeleteProvider()
438 | {
439 | return array(
440 | // required=true
441 | array(
442 | 'result' => array('relationsCount' => 1, 'oldId' => 1, 'containsOld' => false),
443 | 'relationOptions' => array('required' => false),
444 | 'relationAttribute' => array(
445 | array(
446 | 'name' => 'tag_name'
447 | ),
448 | ),
449 | 'comment' => '1 new file'
450 | ),
451 | array(
452 | 'result' => array('relationsCount' => 2, 'oldId' => 1, 'containsOld' => true),
453 | 'relationOptions' => array('required' => false),
454 | 'relationAttribute' => array(
455 | array(
456 | 'id' => 1,
457 | 'name' => 'tag_name1'
458 | ),
459 | array(
460 | 'name' => 'tag_name2'
461 | ),
462 | ),
463 | 'comment' => '1 old, 1 new file'
464 | ),
465 | array(
466 | 'result' => array('relationsCount' => 2, 'oldId' => 1, 'containsOld' => false),
467 | 'relationOptions' => array('required' => false),
468 | 'relationAttribute' => array(
469 | array(
470 | 'name' => 'tag_name1'
471 | ),
472 | array(
473 | 'name' => 'tag_name2'
474 | ),
475 | ),
476 | 'comment' => '2 new files'
477 | ),
478 | array(
479 | 'result' => array('relationsCount' => 0, 'oldId' => 1, 'containsOld' => false),
480 | 'relationOptions' => array('required' => false),
481 | 'relationAttribute' => array(
482 | ),
483 | 'comment' => '0 files'
484 | ),
485 | );
486 | }
487 |
488 | /**
489 | * WFormRelationManyMany::delete
490 | */
491 | public function testDelete()
492 | {
493 | $product = $this->_getProductWithRelation(1);
494 |
495 | $this->assertCount(2, $product->tags);
496 |
497 |
498 | $this->assertTrue($product->delete());
499 |
500 | $sql = "SELECT COUNT(*) FROM products_2_tags WHERE product_id = 1";
501 |
502 | $command = $this->_connection->createCommand($sql);
503 | $this->assertEquals(0, $command->queryScalar());
504 | }
505 |
506 | public function testDeleteWithoutCascade()
507 | {
508 | $product = $this->_getProductWithRelation(1, array('cascadeDelete' => false));
509 |
510 | $this->assertCount(2, $product->tags);
511 |
512 |
513 | $this->assertTrue($product->delete());
514 |
515 | $sql = "SELECT COUNT(*) FROM products_2_tags WHERE product_id = 1";
516 |
517 | $command = $this->_connection->createCommand($sql);
518 | $this->assertEquals(2, $command->queryScalar());
519 | }
520 |
521 | /**
522 | * @param null $id
523 | * @param array $relationOptions
524 | * @return Product
525 | */
526 | protected function _getProductWithRelation($id = null, $relationOptions = array())
527 | {
528 | $product = $id ? Product::model()->findByPk($id) : new Product();
529 | $product->attachBehavior('wform', array(
530 | 'class' => 'WFormBehavior',
531 | 'relations' => array(
532 | 'tags' => $relationOptions,
533 | ),
534 | ));
535 | $product->afterConstruct(new CEvent($product));
536 |
537 | return $product;
538 | }
539 | }
--------------------------------------------------------------------------------
/tests/unit/WFormRelationHasManyTest.php:
--------------------------------------------------------------------------------
1 | markTestSkipped('PDO and SQLite extensions are required.');
13 |
14 | $this->_connection = new CDbConnection('sqlite::memory:');
15 | $this->_connection->active = true;
16 | $this->_connection->pdoInstance->exec(file_get_contents(dirname(__FILE__).'/../fixtures/data/sqlite.sql'));
17 | CActiveRecord::$db = $this->_connection;
18 | }
19 |
20 |
21 | protected function tearDown()
22 | {
23 | $this->_connection->active=false;
24 | }
25 |
26 | /**
27 | * @covers WFormRelationHasMany::setAttributes
28 | */
29 | public function testSetAttributes()
30 | {
31 | $product = $this->_getProductWithRelation();
32 |
33 | $product->attributes = array(
34 | 'name' => 'name',
35 | 'images' => array(
36 | array(
37 | 'object_type' => Attachment::OBJECT_TYPE_PRODUCT_IMAGE,
38 | 'file' => 'somefile.txt',
39 | ),
40 | array(
41 | 'object_type' => Attachment::OBJECT_TYPE_PRODUCT_IMAGE,
42 | 'file' => 'somefile2.txt',
43 | ),
44 | ),
45 | );
46 |
47 | $this->assertCount(2, $product->images);
48 | $this->assertEquals(Attachment::OBJECT_TYPE_PRODUCT_IMAGE, $product->images[0]->object_type);
49 | $this->assertEquals('somefile.txt', $product->images[0]->file);
50 |
51 | $product->attributes = array(
52 | 'name' => 'name',
53 | );
54 | $this->assertCount(2, $product->images);
55 |
56 | $product->attributes = array(
57 | 'name' => 'name',
58 | 'images' => array(),
59 | );
60 | $this->assertCount(0, $product->images);
61 |
62 | $product = $this->_getProductWithRelation(1);
63 |
64 | $product->attributes = array(
65 | 'name' => 'name',
66 | 'images' => array(
67 | array(
68 | 'id' => 1,
69 | 'file' => 'newfile.txt',
70 | ),
71 | ),
72 | );
73 |
74 | $this->assertCount(1, $product->images);
75 | // check if exists images just updated
76 | $this->assertEquals(Attachment::OBJECT_TYPE_PRODUCT_IMAGE, $product->images[0]->object_type);
77 | }
78 |
79 | /**
80 | * @covers WFormRelationHasMany::validate
81 | * @dataProvider validateProvider
82 | */
83 | public function testValidate($expectedResult, $relationOptions, $relationAttribute, $onFailComment = "")
84 | {
85 | $product = $this->_getProductWithRelation(null, $relationOptions);
86 |
87 | $product->attributes = array(
88 | 'name' => 'name',
89 | 'images' => $relationAttribute,
90 | );
91 |
92 | $this->assertEquals($expectedResult, $product->validate(), $onFailComment);
93 | }
94 |
95 | /**
96 | * @covers WFormRelationHasMany::validate
97 | */
98 | public function testUnsetInvalid()
99 | {
100 | $product = $this->_getProductWithRelation(null, array('unsetInvalid' => true));
101 |
102 | $product->attributes = array(
103 | 'name' => 'name',
104 | 'images' => array(
105 | array(
106 | 'object_type' => Attachment::OBJECT_TYPE_PRODUCT_IMAGE,
107 | 'file' => 'somefile.txt'
108 | ),
109 | array(
110 | 'object_type' => Attachment::OBJECT_TYPE_PRODUCT_IMAGE,
111 | 'file' => ''
112 | ),
113 | ),
114 | );
115 |
116 | $this->assertTrue($product->validate());
117 | $this->assertCount(1, $product->images);
118 | }
119 |
120 | public function validateProvider()
121 | {
122 | return array(
123 | // required=true
124 | array(
125 | 'result' => true,
126 | 'relationOptions' => array('required' => true),
127 | 'relationAttribute' => array(
128 | array(
129 | 'object_type' => Attachment::OBJECT_TYPE_PRODUCT_IMAGE,
130 | 'file' => 'somefile.txt'
131 | ),
132 | ),
133 | 'comment' => 'required, 1 valid related object'
134 | ),
135 | array(
136 | 'result' => false,
137 | 'relationOptions' => array('required' => true),
138 | 'relationAttribute' => array(
139 | array(
140 | 'object_type' => Attachment::OBJECT_TYPE_PRODUCT_IMAGE,
141 | 'file' => ''
142 | ),
143 | ),
144 | 'comment' => 'required, 1 invalid related object'
145 | ),
146 | array(
147 | 'result' => false,
148 | 'relationOptions' => array('required' => true),
149 | 'relationAttribute' => array(),
150 | 'comment' => 'required, 0 related objects'
151 | ),
152 | // required=false
153 | array(
154 | 'result' => true,
155 | 'relationOptions' => array('required' => false),
156 | 'relationAttribute' => array(
157 | array(
158 | 'object_type' => Attachment::OBJECT_TYPE_PRODUCT_IMAGE,
159 | 'file' => 'somefile.txt'
160 | ),
161 | ),
162 | 'comment' => 'not required, 1 valid related object'
163 | ),
164 | array(
165 | 'result' => false,
166 | 'relationOptions' => array('required' => false),
167 | 'relationAttribute' => array(
168 | array(
169 | 'object_type' => Attachment::OBJECT_TYPE_PRODUCT_IMAGE,
170 | 'file' => ''
171 | ),
172 | ),
173 | 'comment' => 'not required, 1 invalid related objects'
174 | ),
175 | array(
176 | 'result' => true,
177 | 'relationOptions' => array('required' => false),
178 | 'relationAttribute' => array(),
179 | 'comment' => 'not required, 0 related objects'
180 | ),
181 | // 'unsetInvalid' => true
182 | array(
183 | 'result' => true,
184 | 'relationOptions' => array('required' => false, 'unsetInvalid' => true),
185 | 'relationAttribute' => array(
186 | array(
187 | 'object_type' => Attachment::OBJECT_TYPE_PRODUCT_IMAGE,
188 | 'file' => ''
189 | ),
190 | ),
191 | 'comment' => 'unsetInvalid, not required, 1 invalid related objects'
192 | ),
193 | array(
194 | 'result' => true,
195 | 'relationOptions' => array('required' => true, 'unsetInvalid' => true),
196 | 'relationAttribute' => array(
197 | array(
198 | 'object_type' => Attachment::OBJECT_TYPE_PRODUCT_IMAGE,
199 | 'file' => ''
200 | ),
201 | ),
202 | 'comment' => 'unsetInvalid, required, 1 invalid related objects'
203 | ),
204 | );
205 | }
206 |
207 | /**
208 | * @covers WFormRelationHasMany::save
209 | * @dataProvider saveProvider
210 | */
211 | public function testSave($expectedResult, $relationOptions, $relationAttribute, $onFailComment = "")
212 | {
213 | $product = $this->_getProductWithRelation(null, $relationOptions);
214 |
215 | $product->attributes = array(
216 | 'name' => 'name',
217 | 'images' => $relationAttribute,
218 | );
219 |
220 | $this->assertEquals($expectedResult['saved'], $product->save(), $onFailComment);
221 | $this->assertCount($expectedResult['relationsCount'], $product->images, $onFailComment);
222 | }
223 |
224 | public function saveProvider()
225 | {
226 | return array(
227 | // required=true
228 | array(
229 | 'result' => array('saved' => true, 'relationsCount' => 1),
230 | 'relationOptions' => array('required' => true),
231 | 'relationAttribute' => array(
232 | array(
233 | 'object_type' => Attachment::OBJECT_TYPE_PRODUCT_IMAGE,
234 | 'file' => 'somefile.txt'
235 | ),
236 | ),
237 | 'comment' => 'required, 1 valid related object'
238 | ),
239 | array(
240 | 'result' => array('saved' => false, 'relationsCount' => 1),
241 | 'relationOptions' => array('required' => true),
242 | 'relationAttribute' => array(
243 | array(
244 | 'object_type' => Attachment::OBJECT_TYPE_PRODUCT_IMAGE,
245 | 'file' => ''
246 | ),
247 | ),
248 | 'comment' => 'required, 1 invalid related object'
249 | ),
250 | array(
251 | 'result' => array('saved' => false, 'relationsCount' => 0),
252 | 'relationOptions' => array('required' => true),
253 | 'relationAttribute' => array(),
254 | 'comment' => 'required, 0 related objects'
255 | ),
256 | // required=false
257 | array(
258 | 'result' => array('saved' => true, 'relationsCount' => 1),
259 | 'relationOptions' => array('required' => false),
260 | 'relationAttribute' => array(
261 | array(
262 | 'object_type' => Attachment::OBJECT_TYPE_PRODUCT_IMAGE,
263 | 'file' => 'somefile.txt'
264 | ),
265 | ),
266 | 'comment' => 'not required, 1 valid related object'
267 | ),
268 | array(
269 | 'result' => array('saved' => false, 'relationsCount' => 1),
270 | 'relationOptions' => array('required' => false),
271 | 'relationAttribute' => array(
272 | array(
273 | 'object_type' => Attachment::OBJECT_TYPE_PRODUCT_IMAGE,
274 | 'file' => ''
275 | ),
276 | ),
277 | 'comment' => 'not required, 1 invalid related objects'
278 | ),
279 | array(
280 | 'result' => array('saved' => true, 'relationsCount' => 0),
281 | 'relationOptions' => array('required' => false),
282 | 'relationAttribute' => array(),
283 | 'comment' => 'not required, 0 related objects'
284 | ),
285 | // 'unsetInvalid' => true
286 | array(
287 | 'result' => array('saved' => true, 'relationsCount' => 0),
288 | 'relationOptions' => array('required' => false, 'unsetInvalid' => true),
289 | 'relationAttribute' => array(
290 | array(
291 | 'object_type' => Attachment::OBJECT_TYPE_PRODUCT_IMAGE,
292 | 'file' => ''
293 | ),
294 | ),
295 | 'comment' => 'unsetInvalid, not required, 1 invalid related objects'
296 | ),
297 | array(
298 | 'result' => array('saved' => true, 'relationsCount' => 0),
299 | 'relationOptions' => array('required' => true, 'unsetInvalid' => true),
300 | 'relationAttribute' => array(
301 | array(
302 | 'object_type' => Attachment::OBJECT_TYPE_PRODUCT_IMAGE,
303 | 'file' => ''
304 | ),
305 | ),
306 | 'comment' => 'unsetInvalid, required, 1 invalid related objects'
307 | ),
308 | );
309 | }
310 |
311 | /**
312 | * @covers WFormRelationHasMany::save
313 | */
314 | public function testSaveIfNotSet()
315 | {
316 | $product = Product::model() ;
317 | $product->attachBehavior('wform', array(
318 | 'class' => 'WFormBehavior',
319 | 'relations' => array(
320 | 'images' => array('required' => false),
321 | ),
322 | ));
323 | $product = $product->findByPk(1);
324 | $product->attachBehavior('wform', array(
325 | 'class' => 'WFormBehavior',
326 | 'relations' => array(
327 | 'images' => array('required' => false),
328 | ),
329 | ));
330 | $product->afterFind(new CEvent($product));
331 |
332 |
333 | $this->assertEquals(true, $product->save());
334 | $this->assertCount(1, $product->images);
335 |
336 | $product = Product::model();
337 | $product->attachBehavior('wform', array(
338 | 'class' => 'WFormBehavior',
339 | 'relations' => array(
340 | 'images' => array('required' => false),
341 | ),
342 | ));
343 | $product = $product->with('images')->findByPk(1);
344 | $product->attachBehavior('wform', array(
345 | 'class' => 'WFormBehavior',
346 | 'relations' => array(
347 | 'images' => array('required' => false),
348 | ),
349 | ));
350 | $product->afterFind(new CEvent($product));
351 |
352 | $this->assertEquals(true, $product->save());
353 | $this->assertCount(0, $product->images);
354 | }
355 |
356 | /**
357 | * @covers WFormRelationHasMany::getRelatedModels
358 | */
359 | public function testGetRelatedModels()
360 | {
361 | $product = $this->_getProductWithRelation();
362 | $relation = WFormRelation::getInstance($product, 'images');
363 |
364 | $this->assertCount(0, $relation->getRelatedModels());
365 |
366 | $product->attributes = array(
367 | 'name' => 'name',
368 | 'images' => array(
369 | array(
370 | 'id' => 1,
371 | 'file' => 'newfile.txt',
372 | ),
373 | ),
374 | );
375 |
376 | $this->assertCount(1, $relation->getRelatedModels());
377 |
378 | $product = $this->_getProductWithRelation(1);
379 | $relation = WFormRelation::getInstance($product, 'images');
380 |
381 | $this->assertCount(1, $relation->getRelatedModels());
382 | }
383 |
384 | /**
385 | * @covers WFormRelationHasMany::getActualRelatedModels
386 | */
387 | public function testGetActualRelatedModels()
388 | {
389 | $product = $this->_getProductWithRelation(1);
390 | $relation = WFormRelation::getInstance($product, 'images');
391 |
392 | $this->assertCount(1, $relation->getRelatedModels());
393 | $this->assertCount(1, $relation->getActualRelatedModels());
394 |
395 | $product->attributes = array(
396 | 'name' => 'name',
397 | 'images' => array(),
398 | );
399 |
400 | $this->assertCount(1, $relation->getActualRelatedModels());
401 | $this->assertCount(0, $relation->getRelatedModels());
402 |
403 | $product->attributes = array(
404 | 'name' => 'name',
405 | 'images' => array(
406 | array(
407 | 'file' => 'newfile1.txt',
408 | ),
409 | array(
410 | 'file' => 'newfile2.txt',
411 | ),
412 | ),
413 | );
414 |
415 | $this->assertCount(2, $relation->getRelatedModels());
416 | $this->assertCount(1, $relation->getActualRelatedModels());
417 | }
418 |
419 | /**
420 | * WFormRelationHasMany::lazyDelete
421 | * @dataProvider lazyDeleteProvider
422 | */
423 | public function testLazyDelete($expectedResult, $relationOptions, $relationAttribute, $onFailComment = "")
424 | {
425 | $product = $this->_getProductWithRelation(1, $relationOptions);
426 |
427 | $product->attributes = array(
428 | 'name' => 'name',
429 | 'images' => $relationAttribute,
430 | );
431 |
432 | $this->assertCount($expectedResult['relationsCount'], $product->images, $onFailComment);
433 |
434 | $product->save();
435 |
436 | $this->assertCount($expectedResult['relationsCount'], $product->images, $onFailComment);
437 | $this->assertCount($expectedResult['relationsCount'], $product->getRelated('images', true), $onFailComment);
438 |
439 | $relatedIds = array();
440 | foreach($product->getRelated('images', true) as $model) {
441 | $relatedIds[] = $model->primaryKey;
442 | }
443 |
444 | $this->assertTrue(in_array($expectedResult['oldId'], $relatedIds) == $expectedResult['containsOld']);
445 | }
446 |
447 | public function lazyDeleteProvider()
448 | {
449 | return array(
450 | // required=true
451 | array(
452 | 'result' => array('relationsCount' => 1, 'oldId' => 1, 'containsOld' => false),
453 | 'relationOptions' => array('required' => false),
454 | 'relationAttribute' => array(
455 | array(
456 | 'object_type' => Attachment::OBJECT_TYPE_PRODUCT_IMAGE,
457 | 'file' => 'somefile.txt'
458 | ),
459 | ),
460 | 'comment' => '1 new file'
461 | ),
462 | array(
463 | 'result' => array('relationsCount' => 2, 'oldId' => 1, 'containsOld' => true),
464 | 'relationOptions' => array('required' => false),
465 | 'relationAttribute' => array(
466 | array(
467 | 'id' => 1,
468 | 'object_type' => Attachment::OBJECT_TYPE_PRODUCT_IMAGE,
469 | 'file' => 'somefile.txt'
470 | ),
471 | array(
472 | 'object_type' => Attachment::OBJECT_TYPE_PRODUCT_IMAGE,
473 | 'file' => 'somefile2.txt'
474 | ),
475 | ),
476 | 'comment' => '1 old, 1 new file'
477 | ),
478 | array(
479 | 'result' => array('relationsCount' => 2, 'oldId' => 1, 'containsOld' => false),
480 | 'relationOptions' => array('required' => false),
481 | 'relationAttribute' => array(
482 | array(
483 | 'object_type' => Attachment::OBJECT_TYPE_PRODUCT_IMAGE,
484 | 'file' => 'somefile.txt'
485 | ),
486 | array(
487 | 'object_type' => Attachment::OBJECT_TYPE_PRODUCT_IMAGE,
488 | 'file' => 'somefile.txt'
489 | ),
490 | ),
491 | 'comment' => '2 new files'
492 | ),
493 | array(
494 | 'result' => array('relationsCount' => 0, 'oldId' => 1, 'containsOld' => false),
495 | 'relationOptions' => array('required' => false),
496 | 'relationAttribute' => array(
497 | ),
498 | 'comment' => '0 files'
499 | ),
500 | );
501 | }
502 |
503 | /**
504 | * WFormRelationHasMany::delete
505 | */
506 | public function testDelete()
507 | {
508 | $product = $this->_getProductWithRelation(1);
509 |
510 | $this->assertCount(1, $product->images);
511 | $id = $product->images[0]->primaryKey;
512 | $this->assertTrue($product->delete());
513 | $this->assertEmpty(Attachment::model()->findByPk($id));
514 | }
515 |
516 | /**
517 | * WFormRelationHasMany::delete
518 | */
519 | public function testDeleteWithoutCascade()
520 | {
521 | $product = $this->_getProductWithRelation(1, array('cascadeDelete' => false));
522 |
523 | $this->assertCount(1, $product->images);
524 | $id = $product->images[0]->primaryKey;
525 | $this->assertTrue($product->delete());
526 | $this->assertNotEmpty(Attachment::model()->findByPk($id));
527 | }
528 |
529 | /**
530 | * @param null $id
531 | * @param array $relationOptions
532 | * @return Product
533 | */
534 | protected function _getProductWithRelation($id = null, $relationOptions = array())
535 | {
536 | $product = $id ? Product::model()->findByPk($id) : new Product();
537 | $product->attachBehavior('wform', array(
538 | 'class' => 'WFormBehavior',
539 | 'relations' => array(
540 | 'images' => $relationOptions,
541 | ),
542 | ));
543 | $product->afterConstruct(new CEvent($product));
544 |
545 | return $product;
546 | }
547 | }
--------------------------------------------------------------------------------