├── .gitignore ├── LICENSE.md ├── README.md ├── codeception.yml ├── composer.json ├── docs ├── import-advanced.md ├── import-basic.md └── import-basics.md ├── src ├── behaviors │ └── CellBehavior.php ├── components │ ├── Attribute.php │ ├── Model.php │ ├── StandardAttribute.php │ └── StandardModel.php ├── exceptions │ └── StandardAttributeException.php ├── export │ ├── basic │ │ ├── Attribute.php │ │ ├── Exporter.php │ │ ├── Model.php │ │ ├── StandardAttribute.php │ │ └── StandardModel.php │ └── exceptions │ │ └── ExportException.php ├── helpers │ └── PHPExcelHelper.php └── import │ ├── BaseImporter.php │ ├── CellParser.php │ ├── DI.php │ ├── advanced │ ├── Attribute.php │ ├── Importer.php │ ├── Model.php │ └── StandardModel.php │ ├── basic │ ├── Attribute.php │ ├── Importer.php │ ├── Model.php │ ├── StandardAttribute.php │ └── StandardModel.php │ └── exceptions │ ├── CellException.php │ ├── ImportException.php │ └── RowException.php └── tests ├── _bootstrap.php ├── _data ├── Answer.php ├── Author.php ├── Question.php ├── Test.php └── dump.sql ├── _support └── UnitHelper.php ├── unit.suite.yml └── unit ├── AdvancedImporterTest.php ├── BasicExporterTest.php ├── BasicImporterTest.php ├── _bootstrap.php └── _config.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.lock 3 | 4 | /tests/_output 5 | /tests/unit/UnitTester.php 6 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Yii 2 Excel extension for Yii 2 framework is free software. 2 | It is released under the terms of the following BSD License. 3 | 4 | Copyright © 2015, Alexey Rogachev (https://github.com/arogachev) 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | * Neither the name of test nor the names of its 18 | contributors may be used to endorse or promote products derived from 19 | this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 25 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 29 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Yii 2 Excel 2 | 3 | ActiveRecord import and export based on PHPExcel for Yii 2 framework. 4 | 5 | This library is mainly designed to import data, export is in the raw condition (even it's working in basic form), 6 | under development and not documented yet. 7 | 8 | The important notes: 9 | 10 | - It uses ActiveRecord models and PHPExcel library, so operating big data requires pretty good hardware, especially RAM. 11 | In case of memory shortage I can advise splitting data into smaller chunks. 12 | - This is not just a wrapper on some PHPExcel methods, it's a tool helping import data from Excel in human readable 13 | form with minimal configuration. 14 | - This is designed for periodical import. 15 | - The library is more effective when working with multiple related models and complex data structures. 16 | 17 | [![Latest Stable Version](https://poser.pugx.org/arogachev/yii2-excel/v/stable)](https://packagist.org/packages/arogachev/yii2-excel) 18 | [![Total Downloads](https://poser.pugx.org/arogachev/yii2-excel/downloads)](https://packagist.org/packages/arogachev/yii2-excel) 19 | [![Latest Unstable Version](https://poser.pugx.org/arogachev/yii2-excel/v/unstable)](https://packagist.org/packages/arogachev/yii2-excel) 20 | [![License](https://poser.pugx.org/arogachev/yii2-excel/license)](https://packagist.org/packages/arogachev/yii2-excel) 21 | 22 | - [Installation](#installation) 23 | - [Import Basics](docs/import-basics.md) 24 | - [Basic import](docs/import-basic.md) 25 | - [Advanced import](docs/import-advanced.md) 26 | - [Running import](#running-import) 27 | 28 | ## Installation 29 | 30 | The preferred way to install this extension is through [composer](http://getcomposer.org/download/). 31 | 32 | Either run 33 | 34 | ``` 35 | php composer.phar require --prefer-dist arogachev/yii2-excel 36 | ``` 37 | 38 | or add 39 | 40 | ``` 41 | "arogachev/yii2-excel": "*" 42 | ``` 43 | 44 | to the require section of your `composer.json` file. 45 | 46 | ## Running import 47 | 48 | ```php 49 | if (!$importer->run()) { 50 | echo $importer->error; 51 | 52 | if ($importer->wrongModel) { 53 | echo Html::errorSummary($importer->wrongModel); 54 | } 55 | } 56 | ``` 57 | -------------------------------------------------------------------------------- /codeception.yml: -------------------------------------------------------------------------------- 1 | actor: Tester 2 | paths: 3 | tests: tests 4 | log: tests/_output 5 | data: tests/_data 6 | helpers: tests/_support 7 | settings: 8 | bootstrap: _bootstrap.php 9 | colors: true 10 | memory_limit: 1024M 11 | modules: 12 | config: 13 | Db: 14 | dsn: 'sqlite:tests/_output/temp.db' 15 | user: '' 16 | password: '' 17 | dump: tests/_data/dump.sql 18 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "arogachev/yii2-excel", 3 | "description": "ActiveRecord import and export based on PHPExcel for Yii 2 framework", 4 | "keywords": [ 5 | "yii2", 6 | "excel", 7 | "phpexcel", 8 | "import", 9 | "export", 10 | "active record" 11 | ], 12 | "homepage": "https://github.com/arogachev/yii2-excel", 13 | "type": "yii2-extension", 14 | "license": "BSD-3-Clause", 15 | "authors": [ 16 | { 17 | "name": "Alexey Rogachev", 18 | "email": "arogachev90@gmail.com" 19 | } 20 | ], 21 | "require": { 22 | "yiisoft/yii2": "*", 23 | "phpoffice/phpexcel": "1.8.*" 24 | }, 25 | "require-dev": { 26 | "yiisoft/yii2-codeception": "~2.0" 27 | }, 28 | "autoload": { 29 | "psr-4": { 30 | "arogachev\\excel\\": "src" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /docs/import-advanced.md: -------------------------------------------------------------------------------- 1 | # Advanced import 2 | 3 | Advanced import has all basic import features plus some additional features: 4 | 5 | - Multiple sheets for grouping data 6 | - Multiple models 7 | - Remembering attribute names for each model and redefining them on the fly 8 | - Model defaults 9 | - Linking models through primary keys 10 | - Saving and loading any amount of rows to prevent copy pasting and reduce amount of filling data 11 | 12 | ## Cell types 13 | 14 | There are few types of cells in advanced import: 15 | 16 | - Standard model name. By default it's written in **bold** font. 17 | - Standard attribute name. By default it's written in *italic* font. 18 | - Attribute value. It's written in regular font. 19 | - Defaults section for standard model - standard model name written in **bold** text combined with underline (**___**). 20 | - Saved model link. By default this cell has yellow color filling (HEX code - `#FFFF00`). 21 | - Loaded model link. By default this cell has blue color filling (HEX code - `#00B0F0`). 22 | - Saved rows block. By default this cell has green color filling (HEX code - `#00FF00`). 23 | - Loaded rows block. By default this cell has orange color filling (HEX code - `#F1C232`). 24 | 25 | **Important note:** because of limitations of Github Flavored Markdown, it's impossible to use underlined text or color 26 | filling, so pay attention to notes near the examples. Also please refer to Excel file at the end of this document for 27 | better understanding. 28 | 29 | *Configuration example:* 30 | 31 | ```php 32 | use arogachev\excel\import\advanced\Importer; 33 | use frontend\models\Answer; 34 | use frontend\models\Category; 35 | use frontend\models\Question; 36 | use frontend\models\Test; 37 | use Yii; 38 | use yii\helpers\Html; 39 | 40 | $importer = new Importer([ 41 | 'filePath' => Yii::getAlias('@frontend/data/test.xlsx'), 42 | 'sheetNames' => ['PHP test', 'Courage test'], 43 | 'standardModelsConfig' => [ 44 | [ 45 | 'className' => Test::className(), 46 | 'labels' => ['Test', 'Tests'], 47 | 'standardAttributesConfig' => [ 48 | [ 49 | 'name' => 'type', 50 | 'valueReplacement' => Test::getTypesList(), 51 | ], 52 | [ 53 | 'name' => 'description', 54 | 'valueReplacement' => function ($value) { 55 | return $value ? Html::tag('p', $value) : ''; 56 | }, 57 | ], 58 | [ 59 | 'name' => 'category_id', 60 | 'valueReplacement' => function ($value) { 61 | return Category::find()->select('id')->where(['name' => $value]); 62 | }, 63 | ], 64 | ], 65 | ], 66 | [ 67 | 'className' => Question::className(), 68 | 'labels' => ['Question', 'Questions'], 69 | 'standardAttributesConfig' => [ 70 | [ 71 | 'name' => 'answers_display', 72 | 'valueReplacement' => Question::getAnswersDisplayList(), 73 | ], 74 | ], 75 | ], 76 | [ 77 | 'className' => Answer::className(), 78 | 'labels' => ['Answer', 'Answers'], 79 | 'standardAttributesConfig' => [ 80 | [ 81 | 'name' => 'is_supplemented_by_text', 82 | 'valueReplacement' => Yii::$app->formatter->booleanFormat, 83 | ], 84 | ], 85 | ], 86 | ], 87 | ]); 88 | ``` 89 | 90 | ## Basic filling example 91 | 92 | In advanced import filling of the models of desired type starts with specifying standard model label. Then you need to 93 | specify standard attribute labels horizontally in one row. After that you can fill models with attribute values under 94 | according attribute labels. 95 | 96 | | | A | B | C | D | 97 | | ----- | ---------------- | ------ | ------------------------ | ----------- | 98 | | **1** | **Tests** | | | | 99 | | **2** | *Name* | *Type* | *Description* | *Category* | 100 | | **3** | Temperament test | Closed | This is temperament test | Psychology | 101 | | **4** | PHP test | Closed | | Programming | 102 | | **5** | Git test | Opened | | Programming | 103 | 104 | To switch to filling models of different type just specify other standard model with its attribute labels: 105 | 106 | | | A | B | C | 107 | | ----- |------------- | -------------------------------- | ----------------- | 108 | | **6** | | | | 109 | | **7** | **Question** | | | 110 | | **8** | *Test* | *Content* | *Answers display* | 111 | | **9** | 1 | What PHP frameworks do you know? | Line-by-line | 112 | 113 | When you decide to back to filling models of type that you already used, you can omit attribute labels row, because they 114 | are now remembered and linked to according Excel columns, so you can just write: 115 | 116 | | | A | B | C | D | 117 | | ------ | ------------- | -------- | --- | ------------ | 118 | | **10** | | | | | 119 | | **11** | **Test** | | | | 120 | | **13** | Database test | Opened | | Programming | 121 | 122 | But you can redefine the order and amount of used columns for each standard model at any time on the fly: 123 | 124 | | | A | B | 125 | | ------ | ------------ | ----------- | 126 | | **14** | | | 127 | | **15** | **Test** | | 128 | | **16** | *Name* | *Category* | 129 | | **17** | Courage test | Psychology | 130 | | **18** | PHP test | Programming | 131 | | **19** | Git test | Programming | 132 | 133 | ## Working with relational data 134 | 135 | We can remember any model like this: 136 | 137 | | | A | B | C | D | E | 138 | | ----- | -------- | ------ | ----------------------------- | ----------- | -------- | 139 | | **1** | **Test** | | | | | 140 | | **2** | *Name* | *Type* | *Description* | *Category* | | 141 | | **3** | PHP test | Closed | How good are your PHP skills? | Programming | PHP test | 142 | 143 | The saved model link (cell `E3`) must be located right after the last filled attribute column and have **blue filling** 144 | (you can override that). You need to specify label of saved link, in this case it matches the `name` attribute of the 145 | model. The label can have any name that your want and only used in Excel file for linking purpose. 146 | 147 | Later you can retrieve that link and use it like that: 148 | 149 | | | A | B | C | 150 | | ----- |------------- | -------------------------------- | ----------------- | 151 | | **4** | | | | 152 | | **5** | **Question** | | | 153 | | **6** | *Test* | *Content* | *Answers display* | 154 | | **7** | PHP test | What PHP frameworks do you know? | Line-by-line | 155 | 156 | The cell `A9` must have **yellow filling** (you can override that), otherwise it will be treated as value, not link. 157 | 158 | As a result, after saving linked model primary key value will be fetched and assigned to this attribute. 159 | 160 | Obviously before marking cell as linked to other model primary key, you need to mark according model for saving above 161 | (in case of the same sheet) or in previous sheets. 162 | 163 | Also, when you switch to filling model of different type, the link to last filled model of previous used type is 164 | remembered automatically, so to use it you need just mark the cell as loaded model link and don't write the label: 165 | 166 | | | A | B | C | D | 167 | | ----- | ------------ | -------------------------------- | ----------------------------- | ----------- | 168 | | **1** | **Test** | | | | 169 | | **2** | *Name* | *Type* | *Description* | *Category* | 170 | | **3** | PHP test | Closed | How good are your PHP skills? | Programming | 171 | | **4** | | | | | 172 | | **5** | **Question** | | | | 173 | | **6** | *Test* | *Content* | *Answers display* | | 174 | | **7** | | What PHP frameworks do you know? | Line-by-line | | 175 | 176 | The cell `A7` must have **yellow filling** (you can override that), otherwise it will be treated as value, not link. 177 | 178 | ## Model defaults 179 | 180 | You can specify default values for attributes of each standard model right in Excel sheet, no additional configuration 181 | needed. Filling defaults is similar to filling models, but you need to mark standard model label as the beginning of the 182 | defaults section. It must be written in **bold** text combined with underline (**___**). 183 | 184 | First, it's useful for frequently used values or for a set of identical values: 185 | 186 | | | A | B | 187 | | ----- | -------- | ----------- | 188 | | **1** | **Test** | | 189 | | **2** | *Type* | *Category* | 190 | | **3** | Opened | Programming | 191 | 192 | The text cell `A1` must also be underlined (**___**, you can override that), otherwise it will be treated as the 193 | beginning of filling regular values, not defaults. 194 | 195 | When you mark cell as standard model defaults label, the filling mode is switched to filling defaults, so make sure to 196 | write standard model label again below to fill regular values. 197 | 198 | So if we want to fill a set of programming tests, we can completely skip filling `Category` column. And if majority of 199 | tests have `opened` type, we can skip the filling of these values and fill `Type` column cells only for tests which type 200 | is different (for example `closed` type): 201 | 202 | | | A | B | C | 203 | | ----- | ------------- | ------ | ------------- | 204 | | **4** | | | | 205 | | **5** | **Tests** | | | 206 | | **6** | *Name* | *Type* | *Description* | 207 | | **7** | Git test | | | 208 | | **8** | Database test | | | 209 | | **9** | PHP test | Closed | | 210 | 211 | You can specify defaults at any moment that you want and redefine as many times as you want. If you want to add one more 212 | default value, you don't need to copy paste previous default values (they are remembered already), just write needed 213 | column with value: 214 | 215 | | | A | 216 | | ------ | ------------------------------------------------- | 217 | | **9** | | 218 | | **10** | **Test** | 219 | | **11** | *Description* | 220 | | **12** | This test was created by professional programmer. | 221 | 222 | To clean the default value just leave the cell empty in the next defaults section: 223 | 224 | | | A | 225 | | ------ | ---------- | 226 | | **13** | | 227 | | **14** | **Test** | 228 | | **15** | *Category* | 229 | | **16** | | 230 | 231 | But the biggest advantage of that is you can define the relationships of the models in defaults, and combining with the 232 | right order or models, we can completely eliminate filling relationships every time, which is of course more 233 | user-friendly. 234 | 235 | For example, test consists of questions, and each question can contain set of answers, so we can write the following 236 | defaults: 237 | 238 | | | A | 239 | | ----- | ------------ | 240 | | **1** | **Question** | 241 | | **2** | *Test* | 242 | | **3** | | 243 | | **4** | | 244 | | **5** | **Answer** | 245 | | **6** | *Question* | 246 | | **7** | | 247 | 248 | Cells `A3` and `A6` is marked as loaded model links (yellow filling is used by default, you can override that). 249 | 250 | So if we design our filling to fill tests with its content one by one (one test per file or one per sheet or one after 251 | another), and order of models to be like so: test - question - followed by its answers - next question - followed by 252 | its answers, etc. which is more natural for content manager, we can completely forget about setting relationships now 253 | and just fill the data. 254 | 255 | It's recommended to move defaults to separate sheet and place it before the others and provide this template to content 256 | managers. 257 | 258 | | | A | B | C | D | 259 | | ------ | -------------------------------- | ------------------------- | ---------------------------------- | ----------- | 260 | | **1** | **Test** | | | | 261 | | **2** | *Name* | *Type* | *Description* | *Category* | 262 | | **3** | PHP test | Closed | This test show good are you at PHP | Programming | 263 | | **4** | | | | | 264 | | **5** | **Question** | | | | 265 | | **6** | *Content* | *Display* | | | 266 | | **7** | What PHP frameworks do you know? | Line-by-line | | | 267 | | **8** | | | | | 268 | | **9** | **Answers** | | | | 269 | | **10** | *Content* | *Is supplemented by text* | | | 270 | | **11** | Yii2 | | | | 271 | | **12** | Laravel 5 | | | | 272 | | **13** | Symfony 2 | | | | 273 | | **14** | Other (specify) | Yes | | | 274 | | **15** | | | | | 275 | | **16** | **Question** | | | | 276 | | **17** | Do you use VCS in your work? | | | | 277 | | **18** | | | | | 278 | | **19** | **Answers** | | | | 279 | | **20** | Yes | | | | 280 | | **21** | No | | | | 281 | 282 | Full filling example is available [here](https://docs.google.com/spreadsheets/d/1WQp1JkQNU8tAxX1nMg7rEd_G0kqkaqIVeFx1CjHWHgM/edit?usp=sharing). 283 | -------------------------------------------------------------------------------- /docs/import-basic.md: -------------------------------------------------------------------------------- 1 | # Basic import 2 | 3 | *Features:* 4 | 5 | - Using attribute labels from model or custom labels 6 | - Arbitrary amount and order of columns with attributes 7 | - Create and update mode 8 | - Value replacement 9 | - Detailed error messages with exact wrong filled cell mentioning 10 | - Getting wrong model (where import failed) for getting all validation errors or printing error summary 11 | 12 | *Configuration example:* 13 | 14 | ```php 15 | use arogachev\excel\import\basic\Importer; 16 | use frontend\models\Category; 17 | use frontend\models\Test; 18 | use Yii; 19 | use yii\helpers\Html; 20 | 21 | $importer = new Importer([ 22 | 'filePath' => Yii::getAlias('@frontend/data/test.xlsx'), 23 | 'standardModelsConfig' => [ 24 | [ 25 | 'className' => Test::className(), 26 | 'standardAttributesConfig' => [ 27 | [ 28 | 'name' => 'type', 29 | 'valueReplacement' => Test::getTypesList(), 30 | ], 31 | [ 32 | 'name' => 'description', 33 | 'valueReplacement' => function ($value) { 34 | return $value ? Html::tag('p', $value) : ''; 35 | }, 36 | ], 37 | [ 38 | 'name' => 'category_id', 39 | 'valueReplacement' => function ($value) { 40 | return Category::find()->select('id')->where(['name' => $value]); 41 | }, 42 | ], 43 | ], 44 | ], 45 | ], 46 | ]); 47 | ``` 48 | 49 | *Filling example:* 50 | 51 | | | A | B | C | D | E | 52 | | ----- | --- | ---------------- | ------ | ------------------------ | ----------- | 53 | | **1** | ID | Name | Type | Description | Category | 54 | | **2** | 1 | Temperament test | Closed | This is temperament test | Psychology | 55 | | **3** | | PHP test | Closed | | Programming | 56 | | **4** | | Git test | Opened | | Programming | 57 | 58 | - Attribute names / labels must be placed in first filled row. 59 | - When primary key is specified, then it's update mode, otherwise - create mode. 60 | - For composite primary keys - you always need to specify them fully. 61 | - Completely empty rows are skipped. 62 | -------------------------------------------------------------------------------- /docs/import-basics.md: -------------------------------------------------------------------------------- 1 | # Basics 2 | 3 | This library works with ActiveRecord models. 4 | 5 | There are 4 main types of objects, first two of them are configurable. 6 | 7 | ## Standard Model 8 | 9 | Base model to use. It contains relations to the actual ActiveRecord model class and to list of used standard attributes. 10 | 11 | Common properties: 12 | 13 | - `className` - related ActiveRecord model class name. Required. It's recommended to use `className()` static method of 14 | `yii\db\ActiveRecord` to get it. For example: `Post::className()`. 15 | - `useAttributeLabels` - either to use user-friendly attribute labels or raw names (according database column names). 16 | Defaults to `true`. Using this you can avoid writing `author_id` and write `Author` instead and use translations for 17 | your language. 18 | - `extendStandardAttributes` - extend user defined standard attributes with missing attributes of this model. Defaults 19 | to `true`. 20 | - `standardAttributesConfig` - list of related standard attributes with their configuration. 21 | 22 | Import properties: 23 | 24 | - `setScenario` - set separate scenario for saving models. Defaults to `false`. When using that, scenario is set to 25 | `import`, so you can configure separate validation rules for import. 26 | - `labels` - list of labels used to determine this standard model in Excel data. For advanced import only. It's 27 | recommended to use singular and multiple word forms: `['Post', 'Posts']` so you can write appropriate form depending on 28 | context. 29 | 30 | ## Standard Attribute 31 | 32 | Base attribute belonging to standard model. 33 | 34 | Properties: 35 | 36 | - `name` - related attribute name. Required. 37 | - `label` - user-friendly label. Optional, by default it's taken from ActiveRecord `attributeLabels()` list. 38 | - `valueReplacement` - value mapping to avoid hard coding ids, constants or convert / format value to desired state. 39 | You can specify a list, for example: `Test::getTypesList()`, where `getTypesList` method can be declared like this: 40 | 41 | ``` 42 | 'Closed', 71 | self::TYPE_OPENED => 'Opened', 72 | ]; 73 | } 74 | } 75 | ``` 76 | 77 | or use a callable. 78 | 79 | One way is to use it to get the id of related model from human readable form: 80 | 81 | ```php 82 | function ($value) { 83 | return Category::find()->select('id')->where(['name' => $value]); 84 | }, 85 | ``` 86 | 87 | In this case you must return `ActiveQuery`, not ActiveRecord or set of ActiveRecords, so do not add `->one()` or 88 | `->all()` to the query chain. This is needed to make sure there is exactly one matching value exists. 89 | Specifying attribute in `select` is also important, otherwise first attribute will be selected. 90 | 91 | Another way is just return the new formatted value: 92 | 93 | ```php 94 | function ($value) { 95 | return $value ? Html::tag('p', $value) : ''; 96 | }, 97 | ``` 98 | 99 | In this case we are using this as decorator to wrap text in paragraph tag to prevent writing HTML in Excel. Obviously 100 | it can be more complicated that that. 101 | 102 | ## Model 103 | 104 | Model instance containing data taken from Excel sheet row (and converted, if needed). Belongs to standard model and has 105 | some attributes. 106 | 107 | ## Attribute 108 | 109 | The value of attribute. 110 | -------------------------------------------------------------------------------- /src/behaviors/CellBehavior.php: -------------------------------------------------------------------------------- 1 | 'customInit', 29 | ]; 30 | } 31 | 32 | public function customInit() 33 | { 34 | /* @var $model Attribute */ 35 | $model = $this->owner; 36 | $this->_sheetCodeName = $model->cell->getWorksheet()->getCodeName(); 37 | $this->_cellCoordinate = $model->cell->getCoordinate(); 38 | } 39 | 40 | /** 41 | * @return \PHPExcel_Cell 42 | */ 43 | public function getInitialCell() 44 | { 45 | return DI::getPHPExcel()->getSheetByCodeName($this->_sheetCodeName)->getCell($this->_cellCoordinate); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/components/Attribute.php: -------------------------------------------------------------------------------- 1 | _model = $value; 35 | } 36 | 37 | /** 38 | * @return StandardAttribute 39 | */ 40 | public function getStandardAttribute() 41 | { 42 | return $this->_standardAttribute; 43 | } 44 | 45 | /** 46 | * @param StandardAttribute $value 47 | */ 48 | public function setStandardAttribute($value) 49 | { 50 | $this->_standardAttribute = $value; 51 | } 52 | 53 | /** 54 | * @return mixed 55 | */ 56 | public function getValue() 57 | { 58 | return $this->_value; 59 | } 60 | 61 | /** 62 | * @param mixed $value 63 | */ 64 | public function setValue($value) 65 | { 66 | $this->_value = $value; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/components/Model.php: -------------------------------------------------------------------------------- 1 | initAttributes(); 40 | } 41 | 42 | abstract protected function initAttributes(); 43 | 44 | /** 45 | * @param array $config 46 | */ 47 | protected function initAttribute($config) 48 | { 49 | $className = static::$attributeClassName; 50 | $extendedConfig = array_merge($config, ['model' => $this]); 51 | $this->_attributes[] = new $className($extendedConfig); 52 | } 53 | 54 | /** 55 | * @return StandardModel 56 | */ 57 | public function getStandardModel() 58 | { 59 | return $this->_standardModel; 60 | } 61 | 62 | /** 63 | * @param StandardModel $value 64 | */ 65 | public function setStandardModel($value) 66 | { 67 | $this->_standardModel = $value; 68 | } 69 | 70 | /** 71 | * @return \yii\db\ActiveRecord 72 | */ 73 | public function getInstance() 74 | { 75 | return $this->_instance; 76 | } 77 | 78 | /** 79 | * @param \yii\db\ActiveRecord $value 80 | */ 81 | public function setInstance($value) 82 | { 83 | $this->_instance = $value; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/components/StandardAttribute.php: -------------------------------------------------------------------------------- 1 | validateName(); 46 | 47 | if ($this->_standardModel->extendStandardAttributes && !$this->label) { 48 | $this->label = $this->_standardModel->instance->getAttributeLabel($this->name); 49 | } 50 | 51 | $this->validateLabel(); 52 | $this->validateValueReplacement(); 53 | } 54 | 55 | /** 56 | * @throws StandardAttributeException 57 | */ 58 | protected function validateName() 59 | { 60 | if (!$this->name) { 61 | throw new StandardAttributeException($this, 'Name is required.'); 62 | } 63 | } 64 | 65 | /** 66 | * @throws StandardAttributeException 67 | */ 68 | protected function validateLabel() 69 | { 70 | if ($this->standardModel->useAttributeLabels && !$this->label) { 71 | throw new StandardAttributeException($this, 'Label not specified.'); 72 | } 73 | } 74 | 75 | /** 76 | * @throws StandardAttributeException 77 | */ 78 | protected function validateValueReplacement() 79 | { 80 | if (!$this->valueReplacement || !is_array($this->valueReplacement)) { 81 | return; 82 | } 83 | 84 | if ($this->valueReplacement != array_unique($this->valueReplacement)) { 85 | throw new StandardAttributeException($this, 'Value replacement list contains duplicate labels / values.'); 86 | } 87 | } 88 | 89 | /** 90 | * @return StandardModel 91 | */ 92 | public function getStandardModel() 93 | { 94 | return $this->_standardModel; 95 | } 96 | 97 | /** 98 | * @param StandardModel $value 99 | */ 100 | public function setStandardModel($value) 101 | { 102 | $this->_standardModel = $value; 103 | } 104 | 105 | /** 106 | * @return string 107 | */ 108 | public function getColumn() 109 | { 110 | return $this->_column; 111 | } 112 | 113 | /** 114 | * @param string $value 115 | */ 116 | public function setColumn($value) 117 | { 118 | $this->_column = $value; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/components/StandardModel.php: -------------------------------------------------------------------------------- 1 | initInstance(); 57 | $this->initStandardAttributes(); 58 | $this->validateStandardAttributes(); 59 | $this->indexStandardAttributes(); 60 | } 61 | 62 | /** 63 | * @throws InvalidParamException 64 | */ 65 | protected function initInstance() 66 | { 67 | if (!$this->className) { 68 | throw new InvalidParamException('Class name is required for standard model.'); 69 | } 70 | 71 | $this->_instance = new $this->className; 72 | } 73 | 74 | /** 75 | * @throws InvalidParamException 76 | */ 77 | protected function initStandardAttributes() 78 | { 79 | foreach ($this->standardAttributesConfig as $config) { 80 | $this->initStandardAttribute($config); 81 | } 82 | 83 | if ($this->extendStandardAttributes) { 84 | $existingAttributes = ArrayHelper::getColumn($this->_standardAttributes, 'name'); 85 | $missingAttributes = array_diff($this->getAllowedAttributes(), $existingAttributes); 86 | 87 | foreach ($missingAttributes as $attributeName) { 88 | $this->initStandardAttribute(['name' => $attributeName]); 89 | } 90 | } 91 | } 92 | 93 | /** 94 | * @param array $config 95 | */ 96 | protected function initStandardAttribute($config) 97 | { 98 | $className = static::$standardAttributeClassName; 99 | $extendedConfig = array_merge($config, ['standardModel' => $this]); 100 | $standardAttribute = new $className($extendedConfig); 101 | $this->_standardAttributes[] = $standardAttribute; 102 | } 103 | 104 | protected function validateStandardAttributes() 105 | { 106 | $attributeNames = ArrayHelper::getColumn($this->_standardAttributes, 'name'); 107 | if ($attributeNames != array_unique($attributeNames)) { 108 | throw new InvalidParamException("For standard model \"$this->className\" attribute names are not unique."); 109 | } 110 | 111 | if ($this->useAttributeLabels) { 112 | $attributeLabels = ArrayHelper::getColumn($this->_standardAttributes, 'label'); 113 | if ($attributeLabels != array_unique($attributeLabels)) { 114 | throw new InvalidParamException("For standard model \"$this->className\" attribute labels are not unique."); 115 | } 116 | } 117 | } 118 | 119 | protected function indexStandardAttributes() 120 | { 121 | $propertyName = $this->useAttributeLabels ? 'label' : 'name'; 122 | $standardAttributes = $this->_standardAttributes; 123 | $this->_standardAttributes = []; 124 | 125 | foreach ($standardAttributes as $standardAttribute) { 126 | $this->_standardAttributes[$standardAttribute->{$propertyName}] = $standardAttribute; 127 | } 128 | } 129 | 130 | /** 131 | * @return array 132 | */ 133 | protected function getAllowedAttributes() 134 | { 135 | return $this->_instance->attributes(); 136 | } 137 | 138 | /** 139 | * @return \yii\db\ActiveRecord 140 | */ 141 | public function getInstance() 142 | { 143 | return $this->_instance; 144 | } 145 | 146 | /** 147 | * @return StandardAttribute[] 148 | */ 149 | public function getStandardAttributes() 150 | { 151 | return $this->_standardAttributes; 152 | } 153 | 154 | /** 155 | * @param StandardAttribute[] $value 156 | */ 157 | public function setStandardAttributes($value) 158 | { 159 | $this->_standardAttributes = $value; 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/exceptions/StandardAttributeException.php: -------------------------------------------------------------------------------- 1 | name) { 18 | $attributeName .= " $standardAttribute->name"; 19 | } 20 | 21 | $modelClass = "{$standardAttribute->standardModel->className}"; 22 | $message = "Invalid configuration for $attributeName in model $modelClass. $message"; 23 | 24 | parent::__construct($message, $code, $previous); 25 | } 26 | 27 | /** 28 | * @inheritdoc 29 | */ 30 | public function getName() 31 | { 32 | return 'Standard Attribute Exception'; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/export/basic/Attribute.php: -------------------------------------------------------------------------------- 1 | _standardAttribute->valueReplacement) { 20 | $this->_value = $this->_model->instance->{$this->_standardAttribute->name}; 21 | } else { 22 | $this->replaceValue(); 23 | } 24 | } 25 | 26 | /** 27 | * @throws ExportException 28 | * @throws InvalidParamException 29 | */ 30 | protected function replaceValue() 31 | { 32 | $standardAttribute = $this->_standardAttribute; 33 | $valueReplacement = $standardAttribute->valueReplacement; 34 | if (!$valueReplacement) { 35 | return; 36 | } 37 | 38 | $value = $this->_value; 39 | 40 | if (is_array($standardAttribute->valueReplacement)) { 41 | if (!isset($standardAttribute->valueReplacement[$value])) { 42 | throw new ExportException('Failed to replace value by replacement list.'); 43 | } 44 | 45 | $value = $standardAttribute->valueReplacement[$value]; 46 | } elseif (is_callable($standardAttribute->valueReplacement)) { 47 | $value = call_user_func($standardAttribute->valueReplacement, $this->_model->instance); 48 | } else { 49 | throw new InvalidParamException('$valueReplacement must be specified as array or callable.'); 50 | } 51 | 52 | $this->_value = $value; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/export/basic/Exporter.php: -------------------------------------------------------------------------------- 1 | dataProvider) { 63 | $this->dataProvider->pagination = false; 64 | } 65 | 66 | foreach ($this->standardModelsConfig as $config) { 67 | $this->_standardModels[] = new StandardModel($config); 68 | } 69 | 70 | if (!$this->sheetTitle) { 71 | $this->sheetTitle = function() { 72 | return 'Export ' . date('Y-m-d H:i:s'); 73 | }; 74 | } 75 | } 76 | 77 | public function run() 78 | { 79 | $this->_phpExcel = new PHPExcel; 80 | $sheet = $this->_phpExcel->getActiveSheet(); 81 | 82 | $this->fillModels(); 83 | $this->fillSheetTitle(); 84 | $this->_standardModels[0]->exportAttributeNames($sheet); 85 | 86 | $row = 2; 87 | foreach ($this->_models as $model) { 88 | $model->exportAttributeValues($sheet, $row); 89 | $row++; 90 | } 91 | 92 | $writer = PHPExcel_IOFactory::createWriter($this->_phpExcel, 'Excel2007'); 93 | 94 | if ($this->filePath) { 95 | $filePath = is_callable($this->filePath) ? call_user_func($this->filePath) : $this->filePath; 96 | $writer->save($filePath); 97 | } else { 98 | $fileName = is_callable($this->fileName) ? call_user_func($this->fileName) : $this->fileName; 99 | header('Content-Type: application/ms-excel'); 100 | header("Content-Disposition: attachment;filename=\"$fileName\""); 101 | header('Cache-Control: max-age=0'); 102 | $writer->save('php://output'); 103 | } 104 | } 105 | 106 | protected function fillSheetTitle() 107 | { 108 | $title = is_callable($this->sheetTitle) ? call_user_func($this->sheetTitle) : $this->sheetTitle; 109 | $this->_phpExcel->getActiveSheet()->setTitle($title); 110 | } 111 | 112 | protected function fillModels() 113 | { 114 | $models = $this->dataProvider ? $this->dataProvider->getModels() : $this->query->all(); 115 | foreach ($models as $model) { 116 | $this->_models[] = new Model([ 117 | 'instance' => $model, 118 | 'standardModel' => $this->_standardModels[0], 119 | ]); 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/export/basic/Model.php: -------------------------------------------------------------------------------- 1 | _standardModel->standardAttributes as $standardAttribute) { 23 | $this->initAttribute(['standardAttribute' => $standardAttribute]); 24 | } 25 | } 26 | 27 | /** 28 | * @param \PHPExcel_Worksheet $sheet 29 | * @param integer $row 30 | */ 31 | public function exportAttributeValues($sheet, $row) 32 | { 33 | foreach ($this->_attributes as $attribute) { 34 | $sheet->setCellValue($attribute->standardAttribute->column . $row, $attribute->value); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/export/basic/StandardAttribute.php: -------------------------------------------------------------------------------- 1 | sortStandardAttributes(); 32 | $this->fillStandardAttributesColumns(); 33 | } 34 | 35 | protected function sortStandardAttributes() 36 | { 37 | if (!$this->attributesOrder) { 38 | return; 39 | } 40 | 41 | $standardAttributes = $this->_standardAttributes; 42 | $this->_standardAttributes = []; 43 | 44 | foreach ($this->attributesOrder as $attributeName) { 45 | if (!isset($standardAttributes[$attributeName])) { 46 | $message = "Attribute with name \"$attributeName\" mentioned in \$attributesOrder" 47 | . " for standard model \"$this->className\" not found in standard attributes list"; 48 | throw new ExportException($message); 49 | } 50 | 51 | $this->_standardAttributes[$attributeName] = $standardAttributes[$attributeName]; 52 | } 53 | } 54 | 55 | protected function fillStandardAttributesColumns() 56 | { 57 | $column = 'A'; 58 | 59 | foreach ($this->_standardAttributes as $name => $standardAttribute) { 60 | $standardAttribute->column = $column; 61 | 62 | $column++; 63 | } 64 | } 65 | 66 | /** 67 | * @param \PHPExcel_Worksheet $sheet 68 | */ 69 | public function exportAttributeNames($sheet) 70 | { 71 | $row = 1; 72 | 73 | foreach ($this->_standardAttributes as $name => $standardAttribute) { 74 | $sheet->setCellValue($standardAttribute->column . $row, $name); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/export/exceptions/ExportException.php: -------------------------------------------------------------------------------- 1 | getCellIterator() as $cell) { 14 | if ($cell->getValue()) { 15 | return false; 16 | } 17 | } 18 | 19 | return true; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/import/BaseImporter.php: -------------------------------------------------------------------------------- 1 | filePath) { 67 | throw new InvalidParamException('File path not specified or file not uploaded.'); 68 | } 69 | 70 | if (!file_exists($this->filePath)) { 71 | throw new InvalidParamException("File not exist in path \"$this->filePath\"."); 72 | } 73 | 74 | foreach ($this->standardModelsConfig as $config) { 75 | $this->initStandardModel($config); 76 | } 77 | 78 | $this->configureEventHandlers(); 79 | DI::setImporter($this); 80 | } 81 | 82 | /** 83 | * @param array $config 84 | */ 85 | protected function initStandardModel($config) 86 | { 87 | $this->_standardModels[] = new StandardModel($config); 88 | } 89 | 90 | protected function configureEventHandlers() 91 | { 92 | Event::on(Model::className(), Model::EVENT_INIT, function ($event) { 93 | /* @var $model Model */ 94 | $model = $event->sender; 95 | $model->instance = new $model->standardModel->className; 96 | }); 97 | 98 | $this->on(self::EVENT_RUN, function () { 99 | if (!$this->_models) { 100 | throw new ImportException('No models for import.'); 101 | } 102 | }); 103 | } 104 | 105 | /** 106 | * @return boolean 107 | */ 108 | public function run() 109 | { 110 | try { 111 | $this->safeRun(); 112 | } catch (ImportException $e) { 113 | $this->_error = $e->getMessage(); 114 | 115 | return false; 116 | } 117 | 118 | return true; 119 | } 120 | 121 | protected function safeRun() 122 | { 123 | $this->_phpExcel = PHPExcel_IOFactory::load($this->filePath); 124 | } 125 | 126 | /** 127 | * @return PHPExcel 128 | */ 129 | public function getPhpExcel() 130 | { 131 | return $this->_phpExcel; 132 | } 133 | 134 | /** 135 | * @return string 136 | */ 137 | public function getError() 138 | { 139 | return $this->_error; 140 | } 141 | 142 | /** 143 | * @return \yii\db\ActiveRecord 144 | */ 145 | public function getWrongModel() 146 | { 147 | return $this->_wrongModel; 148 | } 149 | 150 | /** 151 | * @param \yii\db\ActiveRecord $value 152 | */ 153 | public function setWrongModel($value) 154 | { 155 | $this->_wrongModel = $value; 156 | } 157 | 158 | /** 159 | * @return Model[] 160 | */ 161 | public function getModels() 162 | { 163 | return $this->_models; 164 | } 165 | 166 | /** 167 | * @param \PHPExcel_Worksheet_RowIterator|\PHPExcel_Worksheet_Row[] $rows 168 | */ 169 | abstract protected function fillModels($rows); 170 | } 171 | -------------------------------------------------------------------------------- /src/import/CellParser.php: -------------------------------------------------------------------------------- 1 | initDefaults(); 110 | } 111 | 112 | protected function initDefaults() 113 | { 114 | if (!$this->modelLabelDetection) { 115 | $this->modelLabelDetection = function ($cell) { 116 | /* @var $cell PHPExcel_Cell */ 117 | return $cell->getStyle()->getFont()->getBold(); 118 | }; 119 | } 120 | 121 | if (!$this->modelLabelGetting) { 122 | $this->modelLabelGetting = function ($cell) { 123 | /* @var $cell PHPExcel_Cell */ 124 | return $cell->getValue(); 125 | }; 126 | } 127 | 128 | if (!$this->modelDefaultsLabelDetection) { 129 | $this->modelDefaultsLabelDetection = function ($cell) { 130 | /* @var $cell PHPExcel_Cell */ 131 | $isBold = $cell->getStyle()->getFont()->getBold(); 132 | $underline = $cell->getStyle()->getFont()->getUnderline(); 133 | 134 | return $isBold && $underline != 'none'; 135 | }; 136 | } 137 | 138 | if (!$this->modelDefaultsLabelGetting) { 139 | $this->modelDefaultsLabelGetting = function ($cell) { 140 | /* @var $cell PHPExcel_Cell */ 141 | return $cell->getValue(); 142 | }; 143 | } 144 | 145 | if (!$this->attributeNameDetection) { 146 | $this->attributeNameDetection = function ($cell) { 147 | /* @var $cell PHPExcel_Cell */ 148 | return $cell->getStyle()->getFont()->getItalic(); 149 | }; 150 | } 151 | 152 | if (!$this->attributeNameGetting) { 153 | $this->attributeNameGetting = function ($cell) { 154 | /* @var $cell PHPExcel_Cell */ 155 | return $cell->getValue(); 156 | }; 157 | } 158 | 159 | if (!$this->savedPkDetection) { 160 | $this->savedPkDetection = function ($cell) { 161 | /* @var $cell PHPExcel_Cell */ 162 | $startColor = $cell->getStyle()->getFill()->getStartColor()->getRGB(); 163 | $endColor = $cell->getStyle()->getFill()->getEndColor()->getRGB(); 164 | 165 | return $startColor == self::COLOR_BLUE && $endColor == self::COLOR_BLUE; 166 | }; 167 | } 168 | 169 | if (!$this->savedPkGetting) { 170 | $this->savedPkGetting = function ($cell) { 171 | /* @var $cell PHPExcel_Cell */ 172 | return $cell->getValue(); 173 | }; 174 | } 175 | 176 | if (!$this->loadedPkDetection) { 177 | $this->loadedPkDetection = function ($cell) { 178 | /* @var $cell PHPExcel_Cell */ 179 | $startColor = $cell->getStyle()->getFill()->getStartColor()->getRGB(); 180 | $endColor = $cell->getStyle()->getFill()->getEndColor()->getRGB(); 181 | 182 | return $startColor == self::COLOR_YELLOW && $endColor == self::COLOR_YELLOW; 183 | }; 184 | } 185 | 186 | if (!$this->loadedPkGetting) { 187 | $this->loadedPkGetting = function ($cell) { 188 | /* @var $cell PHPExcel_Cell */ 189 | return $cell->getValue(); 190 | }; 191 | } 192 | 193 | if (!$this->savedRowsDetection) { 194 | $this->savedRowsDetection = function ($cell) { 195 | /* @var $cell PHPExcel_Cell */ 196 | $startColor = $cell->getStyle()->getFill()->getStartColor()->getRGB(); 197 | $endColor = $cell->getStyle()->getFill()->getEndColor()->getRGB(); 198 | 199 | return $startColor == self::COLOR_GREEN && $endColor == self::COLOR_GREEN; 200 | }; 201 | } 202 | 203 | if (!$this->savedRowsGetting) { 204 | $this->savedRowsGetting = function ($cell) { 205 | /* @var $cell PHPExcel_Cell */ 206 | return $cell->getValue(); 207 | }; 208 | } 209 | 210 | if (!$this->loadedRowsDetection) { 211 | $this->loadedRowsDetection = function ($cell) { 212 | /* @var $cell PHPExcel_Cell */ 213 | $startColor = $cell->getStyle()->getFill()->getStartColor()->getRGB(); 214 | $endColor = $cell->getStyle()->getFill()->getEndColor()->getRGB(); 215 | 216 | return $startColor == self::COLOR_ORANGE && $endColor == self::COLOR_ORANGE; 217 | }; 218 | } 219 | 220 | if (!$this->loadedRowsGetting) { 221 | $this->loadedRowsGetting = function ($cell) { 222 | /* @var $cell PHPExcel_Cell */ 223 | return $cell->getValue(); 224 | }; 225 | } 226 | } 227 | 228 | /** 229 | * @param PHPExcel_Cell $cell 230 | * @return boolean 231 | */ 232 | public function isModelLabel($cell) 233 | { 234 | return call_user_func($this->modelLabelDetection, $cell); 235 | } 236 | 237 | /** 238 | * @param PHPExcel_Cell $cell 239 | * @return string 240 | * @throws CellException 241 | */ 242 | public function getModelLabel($cell) 243 | { 244 | $value = call_user_func($this->modelLabelGetting, $cell); 245 | if (!$value) { 246 | throw new CellException($cell, 'The model label not specified.'); 247 | } 248 | 249 | return (string) $value; 250 | } 251 | 252 | /** 253 | * @param PHPExcel_Cell $cell 254 | * @return boolean 255 | */ 256 | public function isModelDefaultsLabel($cell) 257 | { 258 | return call_user_func($this->modelDefaultsLabelDetection, $cell); 259 | } 260 | 261 | /** 262 | * @param PHPExcel_Cell $cell 263 | * @return mixed 264 | * @throws CellException 265 | */ 266 | public function getModelDefaultsLabel($cell) 267 | { 268 | $value = call_user_func($this->modelDefaultsLabelGetting, $cell); 269 | if (!$value) { 270 | throw new CellException($cell, 'The model defaults label not specified.'); 271 | } 272 | 273 | return (string) $value; 274 | } 275 | 276 | /** 277 | * @param PHPExcel_Cell $cell 278 | * @return boolean 279 | */ 280 | public function isAttributeName($cell) 281 | { 282 | return call_user_func($this->attributeNameDetection, $cell); 283 | } 284 | 285 | /** 286 | * @param PHPExcel_Cell $cell 287 | * @return string 288 | */ 289 | public function getAttributeName($cell) 290 | { 291 | return (string) call_user_func($this->attributeNameGetting, $cell); 292 | } 293 | 294 | /** 295 | * @param PHPExcel_Cell $cell 296 | * @return boolean 297 | */ 298 | public function isSavedPk($cell) 299 | { 300 | return call_user_func($this->savedPkDetection, $cell); 301 | } 302 | 303 | /** 304 | * @param PHPExcel_Cell $cell 305 | * @return string 306 | * @throws CellException 307 | */ 308 | public function getSavedPk($cell) 309 | { 310 | $value = call_user_func($this->savedPkGetting, $cell); 311 | if (!$value) { 312 | throw new CellException($cell, 'The saved primary key not specified.'); 313 | } 314 | 315 | return (string) $value; 316 | } 317 | 318 | /** 319 | * @param PHPExcel_Cell $cell 320 | * @return boolean 321 | */ 322 | public function isLoadedPk($cell) 323 | { 324 | return call_user_func($this->loadedPkDetection, $cell); 325 | } 326 | 327 | /** 328 | * @param PHPExcel_Cell $cell 329 | * @return string 330 | * @throws CellException 331 | */ 332 | public function getLoadedPk($cell) 333 | { 334 | return (string) call_user_func($this->loadedPkGetting, $cell); 335 | } 336 | 337 | /** 338 | * @param PHPExcel_Cell $cell 339 | * @return boolean 340 | */ 341 | public function isSavedRows($cell) 342 | { 343 | return call_user_func($this->savedRowsDetection, $cell); 344 | } 345 | 346 | /** 347 | * @param PHPExcel_Cell $cell 348 | * @return string 349 | * @throws CellException 350 | */ 351 | public function getSavedRows($cell) 352 | { 353 | $value = call_user_func($this->savedRowsGetting, $cell); 354 | if (!$value) { 355 | throw new CellException($cell, 'Name for saved rows not specified.'); 356 | } 357 | 358 | return (string) $value; 359 | } 360 | 361 | /** 362 | * @param PHPExcel_Cell $cell 363 | * @return boolean 364 | */ 365 | public function isLoadedRows($cell) 366 | { 367 | return call_user_func($this->loadedRowsDetection, $cell); 368 | } 369 | 370 | /** 371 | * @param PHPExcel_Cell $cell 372 | * @return string 373 | * @throws CellException 374 | */ 375 | public function getLoadedRows($cell) 376 | { 377 | $value = call_user_func($this->loadedRowsGetting, $cell); 378 | if (!$value) { 379 | throw new CellException($cell, 'Name for loaded row not specified.'); 380 | } 381 | 382 | return (string) $value; 383 | } 384 | } 385 | -------------------------------------------------------------------------------- /src/import/DI.php: -------------------------------------------------------------------------------- 1 | get('importer'); 19 | } 20 | 21 | /** 22 | * @param BaseImporter $value 23 | */ 24 | public static function setImporter($value) 25 | { 26 | Yii::$container->set('importer', $value); 27 | } 28 | 29 | /** 30 | * @return PHPExcel 31 | * @throws \yii\base\InvalidConfigException 32 | */ 33 | public static function getPHPExcel() 34 | { 35 | return static::getImporter()->phpExcel; 36 | } 37 | 38 | /** 39 | * @return CellParser 40 | * @throws \yii\base\InvalidConfigException 41 | */ 42 | public static function getCellParser() 43 | { 44 | return static::getImporter()->cellParser; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/import/advanced/Attribute.php: -------------------------------------------------------------------------------- 1 | isLoadedPk($this->cell)) { 39 | parent::init(); 40 | } 41 | 42 | $this->trigger(self::EVENT_INIT); 43 | } 44 | 45 | public function linkRelatedModel() 46 | { 47 | $cell = $this->getInitialCell(); 48 | 49 | if (!DI::getCellParser()->isLoadedPk($cell)) { 50 | return; 51 | } 52 | 53 | $loadedPk = DI::getCellParser()->getLoadedPk($cell); 54 | foreach (array_reverse(DI::getImporter()->models) as $model) { 55 | $isRelatedByPk = $loadedPk && $model->savedPk == $loadedPk; 56 | 57 | $attributeModelClass = $this->_standardAttribute->standardModel->className; 58 | $modelClass = $model->standardModel->className; 59 | $isRelatedByLastPk = $loadedPk === '' && $attributeModelClass != $modelClass; 60 | 61 | if ($isRelatedByPk || $isRelatedByLastPk) { 62 | $this->_relatedModel = $model; 63 | 64 | break; 65 | } 66 | } 67 | 68 | if (!$this->_relatedModel) { 69 | throw new CellException($cell, 'Related model not found.'); 70 | } 71 | } 72 | 73 | /** 74 | * @return Model 75 | */ 76 | public function getRelatedModel() 77 | { 78 | return $this->_relatedModel; 79 | } 80 | 81 | /** 82 | * @param Model $value 83 | */ 84 | public function setRelatedModel($value) 85 | { 86 | $this->_relatedModel = $value; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/import/advanced/Importer.php: -------------------------------------------------------------------------------- 1 | validateStandardModelLabels(); 79 | $this->_cellParser = new CellParser($this->cellParserConfig); 80 | } 81 | 82 | /** 83 | * @inheritdoc 84 | */ 85 | protected function initStandardModel($config) 86 | { 87 | $this->_standardModels[] = new StandardModel($config); 88 | } 89 | 90 | /** 91 | * @throws InvalidConfigException 92 | */ 93 | protected function validateStandardModelLabels() 94 | { 95 | $labels = []; 96 | foreach ($this->_standardModels as $standardModel) { 97 | foreach ($standardModel->labels as $label) { 98 | $labels[] = $label; 99 | } 100 | } 101 | 102 | if (count($labels) != count(array_unique($labels))) { 103 | throw new InvalidConfigException('Each standard model label must be unique.'); 104 | } 105 | } 106 | 107 | /** 108 | * @inheritdoc 109 | */ 110 | protected function fillModels($rows) 111 | { 112 | foreach ($rows as $row) { 113 | // Skipping completely empty rows 114 | // Only loaded pk links can be empty 115 | 116 | $isRowEmpty = PHPExcelHelper::isRowEmpty($row); 117 | $row->getCellIterator()->rewind(); 118 | $currentCell = $row->getCellIterator()->current(); 119 | 120 | if (!$row->getCellIterator()->valid()) { 121 | continue; 122 | } 123 | 124 | if ($isRowEmpty && !$this->_cellParser->isLoadedPk($currentCell)) { 125 | continue; 126 | } 127 | 128 | if ($this->_cellParser->isLoadedRows($currentCell)) { 129 | $this->fillModels($this->_savedRows[$this->_cellParser->getLoadedRows($currentCell)]); 130 | 131 | continue; 132 | } 133 | 134 | if ($this->_cellParser->isSavedRows($currentCell)) { 135 | if ($this->_currentSavedRow) { 136 | $this->_currentSavedRow = null; 137 | } else { 138 | $this->_currentSavedRow = $this->_cellParser->getSavedRows($currentCell); 139 | } 140 | 141 | continue; 142 | } 143 | 144 | if ($this->_currentSavedRow) { 145 | $this->_savedRows[$this->_currentSavedRow][] = clone $row; 146 | 147 | continue; 148 | } 149 | 150 | if ($this->_cellParser->isModelDefaultsLabel($currentCell)) { 151 | $this->_currentMode = self::MODE_DEFAULT_ATTRIBUTES; 152 | $this->fillCurrentStandardModel($currentCell); 153 | 154 | continue; 155 | } 156 | 157 | if ($this->_cellParser->isModelLabel($currentCell)) { 158 | $this->_currentMode = self::MODE_IMPORT; 159 | $this->fillCurrentStandardModel($currentCell); 160 | 161 | continue; 162 | } 163 | 164 | if (!$this->_currentStandardModel) { 165 | throw new RowException($row, 'Model label must be declared before attribute names or values'); 166 | } 167 | 168 | if ($this->_cellParser->isAttributeName($currentCell)) { 169 | $this->_currentStandardModel->parseAttributeNames($row); 170 | 171 | continue; 172 | } 173 | 174 | if ($this->_currentMode == self::MODE_IMPORT) { 175 | $this->_models[] = new Model([ 176 | 'row' => $row, 177 | 'standardModel' => $this->_currentStandardModel, 178 | ]); 179 | } elseif ($this->_currentMode == self::MODE_DEFAULT_ATTRIBUTES) { 180 | $this->_currentStandardModel->setDefaultAttributes($row); 181 | } 182 | } 183 | } 184 | 185 | /** 186 | * @inheritdoc 187 | */ 188 | protected function safeRun() 189 | { 190 | parent::safeRun(); 191 | 192 | $sheetNames = $this->sheetNames === '*' ? $this->_phpExcel->getSheetNames() : (array) $this->sheetNames; 193 | foreach ($sheetNames as $sheetName) { 194 | $sheet = $this->_phpExcel->getSheetByName($sheetName); 195 | $this->fillModels($sheet->getRowIterator()); 196 | } 197 | 198 | Yii::$app->db->transaction(function() { 199 | foreach ($this->_models as $model) { 200 | $model->load(); 201 | $model->save(); 202 | } 203 | }); 204 | 205 | $this->trigger(self::EVENT_RUN); 206 | } 207 | 208 | /** 209 | * @param PHPExcel_Cell $cell 210 | * @throws CellException 211 | */ 212 | protected function fillCurrentStandardModel($cell) 213 | { 214 | if ($this->_currentMode == self::MODE_DEFAULT_ATTRIBUTES) { 215 | $label = $this->_cellParser->getModelDefaultsLabel($cell); 216 | } else { 217 | $label = $this->_cellParser->getModelLabel($cell); 218 | } 219 | 220 | foreach ($this->_standardModels as $standardModel) { 221 | if (in_array($label, $standardModel->labels)) { 222 | $this->_currentStandardModel = $standardModel; 223 | 224 | return; 225 | } 226 | } 227 | 228 | throw new CellException($cell, "Standard model not found by given label."); 229 | } 230 | 231 | /** 232 | * @return CellParser 233 | */ 234 | public function getCellParser() 235 | { 236 | return $this->_cellParser; 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /src/import/advanced/Model.php: -------------------------------------------------------------------------------- 1 | initAttributes(); 30 | $this->mergeDefaultAttributes(); 31 | $this->initSavedPk(); 32 | $this->trigger(self::EVENT_INIT); 33 | } 34 | 35 | /** 36 | * @param array $config 37 | */ 38 | protected function initAttribute($config) 39 | { 40 | $attribute = new Attribute($config); 41 | $attribute->linkRelatedModel(); 42 | $this->_attributes[] = $attribute; 43 | } 44 | 45 | protected function initSavedPk() 46 | { 47 | $sheet = $this->row->getCellIterator()->current()->getWorksheet(); 48 | $columns = ArrayHelper::getColumn($this->_standardModel->standardAttributes, 'column'); 49 | sort($columns); 50 | $lastColumn = end($columns); 51 | $cell = $sheet->getCell(++$lastColumn . $this->row->getRowIndex()); 52 | 53 | if (DI::getCellParser()->isSavedPk($cell)) { 54 | $this->_savedPk = DI::getCellParser()->getSavedPk($cell); 55 | } 56 | } 57 | 58 | protected function mergeDefaultAttributes() 59 | { 60 | foreach ($this->_standardModel->defaultAttributes as $defaultAttribute) { 61 | $isFound = false; 62 | foreach ($this->_attributes as $index => $attribute) { 63 | $namesMatch = $defaultAttribute->standardAttribute->name == $attribute->standardAttribute->name; 64 | if (!$namesMatch) { 65 | continue; 66 | } else { 67 | $isFound = true; 68 | } 69 | 70 | $isLoadedPk = DI::getCellParser()->isLoadedPk($attribute->getInitialCell()); 71 | if ($namesMatch && $attribute->value === null && !$isLoadedPk) { 72 | $this->mergeDefaultAttribute($defaultAttribute, $index); 73 | 74 | break; 75 | } 76 | } 77 | 78 | if (!$isFound) { 79 | $this->mergeDefaultAttribute($defaultAttribute); 80 | } 81 | } 82 | } 83 | 84 | /** 85 | * @param Attribute $defaultAttribute 86 | * @param null|integer $index 87 | * @throws \arogachev\excel\import\exceptions\CellException 88 | */ 89 | protected function mergeDefaultAttribute($defaultAttribute, $index = null) 90 | { 91 | $attribute = new Attribute([ 92 | 'standardAttribute' => $defaultAttribute->standardAttribute, 93 | 'cell' => $defaultAttribute->getInitialCell(), 94 | ]); 95 | $attribute->linkRelatedModel(); 96 | $index === null ? $this->_attributes[] = $attribute : $this->_attributes[$index] = $attribute; 97 | } 98 | 99 | public function load() 100 | { 101 | $this->replaceValues(); 102 | 103 | parent::load(); 104 | } 105 | 106 | protected function replaceValues() 107 | { 108 | foreach ($this->_attributes as $attribute) { 109 | if ($attribute->relatedModel) { 110 | $attribute->value = $attribute->relatedModel->instance->primaryKey; 111 | } elseif ($attribute->standardAttribute->valueReplacement) { 112 | $attribute->replaceValue(true); 113 | } 114 | } 115 | } 116 | 117 | /** 118 | * @return string 119 | */ 120 | public function getSavedPk() 121 | { 122 | return $this->_savedPk; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/import/advanced/StandardModel.php: -------------------------------------------------------------------------------- 1 | _defaultAttributes; 29 | } 30 | 31 | /** 32 | * @param \PHPExcel_Worksheet_Row $row 33 | */ 34 | public function setDefaultAttributes($row) 35 | { 36 | $this->_defaultAttributes = []; 37 | $sheet = $row->getCellIterator()->current()->getWorksheet(); 38 | foreach ($this->_standardAttributes as $standardAttribute) { 39 | if ($standardAttribute->column) { 40 | $this->_defaultAttributes[] = new Attribute([ 41 | 'cell' => $sheet->getCell($standardAttribute->column . $row->getRowIndex()), 42 | 'standardAttribute' => $standardAttribute, 43 | ]); 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/import/basic/Attribute.php: -------------------------------------------------------------------------------- 1 | _standardAttribute->valueReplacement) { 33 | $this->_value = $this->cell->getValue(); 34 | } else { 35 | $this->replaceValue(); 36 | } 37 | } 38 | 39 | /** 40 | * @param boolean $throwException 41 | * @throws CellException 42 | * @throws InvalidParamException 43 | */ 44 | public function replaceValue($throwException = false) 45 | { 46 | if ($this->_replaced) { 47 | return; 48 | } 49 | 50 | $valueReplacement = $this->_standardAttribute->valueReplacement; 51 | $cellValue = $this->cell->getValue(); 52 | $value = null; 53 | 54 | if (is_array($valueReplacement)) { 55 | $flippedList = array_flip($valueReplacement); 56 | if (isset($flippedList[$cellValue])) { 57 | $value = $flippedList[$cellValue]; 58 | } elseif ($throwException) { 59 | throw new CellException($this->cell, 'Failed to replace value by replacement list.'); 60 | } 61 | } elseif (is_callable($valueReplacement)) { 62 | $result = call_user_func($valueReplacement, $cellValue); 63 | 64 | if ($result instanceof ActiveQuery) { 65 | $models = $result->all(); 66 | 67 | if (count($models) == 1) { 68 | $value = $models[0]->{$result->select[0]}; 69 | } elseif ($throwException) { 70 | throw new CellException($this->cell, 'Failed to replace value by replacement query.'); 71 | } 72 | } else { 73 | $value = $result; 74 | } 75 | } else { 76 | throw new InvalidParamException('$valueReplacement must be specified as array or callable.'); 77 | } 78 | 79 | if ($value !== null) { 80 | $this->_value = $value; 81 | $this->_replaced = true; 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/import/basic/Importer.php: -------------------------------------------------------------------------------- 1 | _standardModels[0]->parseAttributeNames($row)) { 25 | throw new RowException($row, 'Attribute names must be placed in first filled row.'); 26 | } 27 | } else { 28 | $this->_models[] = new Model([ 29 | 'row' => $row, 30 | 'standardModel' => $this->_standardModels[0], 31 | ]); 32 | } 33 | 34 | $c++; 35 | } 36 | } 37 | 38 | /** 39 | * @inheritdoc 40 | */ 41 | protected function safeRun() 42 | { 43 | parent::safeRun(); 44 | 45 | $this->fillModels($this->_phpExcel->getActiveSheet()->getRowIterator()); 46 | 47 | foreach ($this->_models as $model) { 48 | $model->load(); 49 | $model->validate(); 50 | } 51 | 52 | Yii::$app->db->transaction(function () { 53 | foreach ($this->_models as $model) { 54 | $model->save(); 55 | } 56 | }); 57 | 58 | $this->trigger(self::EVENT_RUN); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/import/basic/Model.php: -------------------------------------------------------------------------------- 1 | trigger(self::EVENT_INIT); 37 | } 38 | 39 | protected function initAttributes() 40 | { 41 | $sheet = $this->row->getCellIterator()->current()->getWorksheet(); 42 | 43 | foreach ($this->_standardModel->standardAttributes as $standardAttribute) { 44 | if ($standardAttribute->column) { 45 | $this->initAttribute([ 46 | 'standardAttribute' => $standardAttribute, 47 | 'cell' => $sheet->getCell($standardAttribute->column . $this->row->getRowIndex()), 48 | ]); 49 | } 50 | } 51 | } 52 | 53 | public function load() 54 | { 55 | $this->loadExisting(); 56 | $this->assignMassively(); 57 | } 58 | 59 | protected function loadExisting() 60 | { 61 | if ($this->isPkEmpty()) { 62 | return; 63 | } 64 | 65 | /* @var $modelClass \yii\db\ActiveRecord */ 66 | $modelClass = $this->_standardModel->className; 67 | $model = $modelClass::findOne($this->getPkValues()); 68 | if ($model) { 69 | $this->_instance = $model; 70 | } 71 | } 72 | 73 | /** 74 | * @return Attribute[] 75 | */ 76 | protected function getPk() 77 | { 78 | $attributes = []; 79 | foreach ($this->_attributes as $attribute) { 80 | if (in_array($attribute->standardAttribute->name, $this->_instance->primaryKey())) { 81 | $attributes[] = $attribute; 82 | } 83 | } 84 | 85 | return $attributes; 86 | } 87 | 88 | /** 89 | * @return array 90 | */ 91 | protected function getPkValues() 92 | { 93 | $values = []; 94 | foreach ($this->getPk() as $attribute) { 95 | $values[$attribute->standardAttribute->name] = $attribute->value; 96 | } 97 | 98 | return $values; 99 | } 100 | 101 | /** 102 | * @return boolean 103 | */ 104 | protected function isPkEmpty() 105 | { 106 | foreach ($this->getPkValues() as $value) { 107 | if ($value) { 108 | return false; 109 | } 110 | } 111 | 112 | return true; 113 | } 114 | 115 | protected function assignMassively() 116 | { 117 | foreach ($this->_attributes as $attribute) { 118 | $this->_instance->{$attribute->standardAttribute->name} = $attribute->value; 119 | } 120 | } 121 | 122 | public function validate() 123 | { 124 | if (!$this->_instance->validate()) { 125 | DI::getImporter()->wrongModel = $this->_instance; 126 | throw new RowException($this->row, 'Model data is not valid.'); 127 | } 128 | } 129 | 130 | /** 131 | * @param boolean $runValidation 132 | * @throws RowException 133 | */ 134 | public function save($runValidation = true) 135 | { 136 | if ($runValidation) { 137 | $this->validate(); 138 | } 139 | 140 | $this->_instance->save(false); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/import/basic/StandardAttribute.php: -------------------------------------------------------------------------------- 1 | _standardModel; 23 | $model = $standardModel->instance; 24 | 25 | if (!in_array($this->name, $model->attributes())) { 26 | throw new StandardAttributeException($this, 'Attribute not exist.'); 27 | } 28 | 29 | if (in_array($this->name, $model->primaryKey())) { 30 | return; 31 | } 32 | 33 | if (in_array($this->name, ArrayHelper::getColumn($standardModel->standardAttributesConfig, 'name'))) { 34 | return; 35 | } 36 | 37 | if (!$model->isAttributeSafe($this->name)) { 38 | throw new StandardAttributeException($this, 'Attribute is not allowed for import.'); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/import/basic/StandardModel.php: -------------------------------------------------------------------------------- 1 | configureEventHandlers(); 49 | } 50 | 51 | /** 52 | * @throws InvalidParamException 53 | */ 54 | protected function initInstance() 55 | { 56 | parent::initInstance(); 57 | 58 | if ($this->setScenario) { 59 | $this->_instance->scenario = self::SCENARIO_IMPORT; 60 | } 61 | } 62 | 63 | protected function configureEventHandlers() 64 | { 65 | if ($this->setScenario) { 66 | foreach (self::$_setScenarioEvents as $eventName) { 67 | Event::on($this->className, $eventName, function ($event) { 68 | $event->sender->scenario = self::SCENARIO_IMPORT; 69 | }); 70 | } 71 | } 72 | } 73 | 74 | /** 75 | * @return array 76 | */ 77 | protected function getAllowedAttributes() 78 | { 79 | $model = $this->_instance; 80 | $attributes = []; 81 | 82 | foreach ($model->attributes() as $attribute) { 83 | if (in_array($attribute, $model->primaryKey()) || $model->isAttributeSafe($attribute)) { 84 | $attributes[] = $attribute; 85 | } 86 | } 87 | 88 | return $attributes; 89 | } 90 | 91 | /** 92 | * @param PHPExcel_Worksheet_Row $row 93 | * @return boolean 94 | * @throws CellException 95 | * @throws RowException 96 | */ 97 | public function parseAttributeNames($row) 98 | { 99 | foreach ($this->_standardAttributes as $standardAttribute) { 100 | $standardAttribute->column = null; 101 | } 102 | 103 | $model = $this->_instance; 104 | $standardAttributeNames = array_keys($this->_standardAttributes); 105 | $attributeNames = []; 106 | 107 | foreach ($row->getCellIterator() as $cell) { 108 | if (!$cell->getValue()) { 109 | continue; 110 | } 111 | 112 | if (!in_array($cell->getValue(), $standardAttributeNames)) { 113 | throw new CellException($cell, "Attribute not exist."); 114 | } 115 | 116 | $this->_standardAttributes[$cell->getValue()]->column = $cell->getColumn(); 117 | $attributeNames[] = $cell->getValue(); 118 | } 119 | 120 | $pk = $model->primaryKey(); 121 | $filledPk = array_intersect($pk, $attributeNames); 122 | 123 | // Primary keys either must not be specified at all (create mode), 124 | // or must be specified fully (update mode, in case of composite primary keys) 125 | if ($filledPk && count($pk) != count($filledPk)) { 126 | throw new RowException($row, 'All primary key attributes must be specified for updating model.'); 127 | } 128 | 129 | return !empty($attributeNames); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/import/exceptions/CellException.php: -------------------------------------------------------------------------------- 1 | getWorksheet()->getTitle(); 16 | $cellCoordinate = $cell->getCoordinate(); 17 | $message = "Error when preparing data for import: sheet \"$sheetTitle\", cell \"$cellCoordinate\". $message"; 18 | 19 | parent::__construct($message, $code, $previous); 20 | } 21 | 22 | /** 23 | * @inheritdoc 24 | */ 25 | public function getName() 26 | { 27 | return 'Cell Exception'; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/import/exceptions/ImportException.php: -------------------------------------------------------------------------------- 1 | getCellIterator()->current()->getWorksheet()->getTitle(); 16 | $message = "Import failed at sheet \"$sheetTitle\", row \"{$row->getRowIndex()}\". $message"; 17 | 18 | parent::__construct($message, $code, $previous); 19 | } 20 | 21 | /** 22 | * @inheritdoc 23 | */ 24 | public function getName() 25 | { 26 | return 'Row Exception'; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/_bootstrap.php: -------------------------------------------------------------------------------- 1 | Question::className(), 'targetAttribute' => 'id'], 31 | ['content', 'string'], 32 | ['sort', 'integer'], 33 | ]; 34 | } 35 | 36 | /** 37 | * @inheritdoc 38 | */ 39 | public function attributeLabels() 40 | { 41 | return [ 42 | 'id' => 'ID', 43 | 'question_id' => 'Question', 44 | ]; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/_data/Author.php: -------------------------------------------------------------------------------- 1 | Test::className(), 'targetAttribute' => 'id'], 31 | ['content', 'string'], 32 | ['sort', 'integer'], 33 | ]; 34 | } 35 | 36 | /** 37 | * @inheritdoc 38 | */ 39 | public function attributeLabels() 40 | { 41 | return [ 42 | 'id' => 'ID', 43 | 'test_id' => 'Test', 44 | ]; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/_data/Test.php: -------------------------------------------------------------------------------- 1 | array_keys(static::getTypesList())], 47 | ['author_id', 'exist', 'targetClass' => Author::className(), 'targetAttribute' => 'id'], 48 | ]; 49 | } 50 | 51 | /** 52 | * @inheritdoc 53 | */ 54 | public function attributeLabels() 55 | { 56 | return [ 57 | 'id' => 'ID', 58 | 'author_id' => 'Author', 59 | ]; 60 | } 61 | 62 | /** 63 | * @return \yii\db\ActiveQuery 64 | */ 65 | public function getAuthor() 66 | { 67 | return $this->hasOne(Author::className(), ['id' => 'author_id']); 68 | } 69 | 70 | /** 71 | * @return array 72 | */ 73 | public static function getTypesList() 74 | { 75 | return [ 76 | self::TYPE_CLOSED => 'Closed', 77 | self::TYPE_OPENED => 'Opened', 78 | ]; 79 | } 80 | 81 | /** 82 | * @return string 83 | */ 84 | public function getTypeLabel() 85 | { 86 | return static::getTypesList()[$this->type]; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /tests/_data/dump.sql: -------------------------------------------------------------------------------- 1 | BEGIN TRANSACTION; 2 | 3 | CREATE TABLE "authors" ( 4 | "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, 5 | "name" text NOT NULL, 6 | "rating" integer NOT NULL 7 | ); 8 | 9 | INSERT INTO "authors" ("id", "name", "rating") VALUES (1, 'Ivan Ivanov', 7); 10 | INSERT INTO "authors" ("id", "name", "rating") VALUES (2, 'Petr Petrov', 9); 11 | 12 | CREATE TABLE "tests" ( 13 | "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, 14 | "name" text NOT NULL, 15 | "type" integer NOT NULL, 16 | "description" text NOT NULL, 17 | "author_id" integer NOT NULL 18 | ); 19 | 20 | INSERT INTO "tests" ("id", "name", "type", "description", "author_id") VALUES (1, 'Common test', 1, '

This is the common test

', 1); 21 | 22 | CREATE TABLE "questions" ( 23 | "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, 24 | "test_id" integer NOT NULL, 25 | "content" text NOT NULL, 26 | "sort" integer NOT NULL, 27 | FOREIGN KEY ("test_id") REFERENCES "tests" ("id") ON DELETE CASCADE ON UPDATE CASCADE 28 | ); 29 | 30 | CREATE TABLE "answers" ( 31 | "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, 32 | "question_id" integer NOT NULL, 33 | "content" text NOT NULL, 34 | "sort" integer NOT NULL, 35 | FOREIGN KEY ("question_id") REFERENCES "questions" ("id") ON DELETE CASCADE ON UPDATE CASCADE 36 | ); 37 | 38 | COMMIT; 39 | -------------------------------------------------------------------------------- /tests/_support/UnitHelper.php: -------------------------------------------------------------------------------- 1 | $path, 27 | 'sheetNames' => ['Data'], 28 | 'standardModelsConfig' => [ 29 | [ 30 | 'className' => Test::className(), 31 | 'labels' => ['Test', 'Tests'], 32 | 'standardAttributesConfig' => [ 33 | [ 34 | 'name' => 'type', 35 | 'valueReplacement' => Test::getTypesList(), 36 | ], 37 | [ 38 | 'name' => 'description', 39 | 'valueReplacement' => function ($value) { 40 | return $value ? Html::tag('p', $value) : ''; 41 | }, 42 | ], 43 | [ 44 | 'name' => 'author_id', 45 | 'valueReplacement' => function ($value) { 46 | return Author::find()->select('id')->where(['name' => $value]); 47 | }, 48 | ], 49 | ], 50 | ], 51 | [ 52 | 'className' => Question::className(), 53 | 'labels' => ['Question', 'Questions'], 54 | ], 55 | [ 56 | 'className' => Answer::className(), 57 | 'labels' => ['Answer', 'Answers'], 58 | ], 59 | ], 60 | ]); 61 | 62 | $result = $importer->run(); 63 | $this->assertEquals($importer->error, null); 64 | $this->assertEquals($result, true); 65 | 66 | $this->assertEquals(Test::find()->count(), 5); 67 | $this->assertEquals(Test::findOne(1)->attributes, [ 68 | 'id' => 1, 69 | 'name' => 'Basic test', 70 | 'type' => 2, 71 | 'description' => '

This is the basic test

', 72 | 'author_id' => 2, 73 | ]); 74 | $this->assertEquals(Test::findOne(2)->attributes, [ 75 | 'id' => 2, 76 | 'name' => 'Common test', 77 | 'type' => 1, 78 | 'description' => '', 79 | 'author_id' => 1, 80 | ]); 81 | $this->assertEquals(Test::findOne(3)->attributes, [ 82 | 'id' => 3, 83 | 'name' => 'Programming test', 84 | 'type' => 2, 85 | 'description' => '', 86 | 'author_id' => 2, 87 | ]); 88 | $this->assertEquals(Test::findOne(4)->attributes, [ 89 | 'id' => 4, 90 | 'name' => 'Language test', 91 | 'type' => 1, 92 | 'description' => '', 93 | 'author_id' => 1, 94 | ]); 95 | $this->assertEquals(Test::findOne(5)->attributes, [ 96 | 'id' => 5, 97 | 'name' => 'Science test', 98 | 'type' => 1, 99 | 'description' => '', 100 | 'author_id' => 1, 101 | ]); 102 | 103 | $this->assertEquals(Question::find()->count(), 5); 104 | $this->assertEquals(Question::findOne(1)->attributes, [ 105 | 'id' => 1, 106 | 'test_id' => 1, 107 | 'content' => "What's your name?", 108 | 'sort' => 1, 109 | ]); 110 | $this->assertEquals(Question::findOne(2)->attributes, [ 111 | 'id' => 2, 112 | 'test_id' => 1, 113 | 'content' => 'How old are you?', 114 | 'sort' => 2, 115 | ]); 116 | $this->assertEquals(Question::findOne(3)->attributes, [ 117 | 'id' => 3, 118 | 'test_id' => 5, 119 | 'content' => "What's your name?", 120 | 'sort' => 1, 121 | ]); 122 | $this->assertEquals(Question::findOne(4)->attributes, [ 123 | 'id' => 4, 124 | 'test_id' => 5, 125 | 'content' => 'How old are you?', 126 | 'sort' => 2, 127 | ]); 128 | $this->assertEquals(Question::findOne(5)->attributes, [ 129 | 'id' => 5, 130 | 'test_id' => 1, 131 | 'content' => 'Yes or no?', 132 | 'sort' => 1, 133 | ]); 134 | 135 | $this->assertEquals(Answer::find()->count(), 2); 136 | $this->assertEquals(Answer::findOne(1)->attributes, [ 137 | 'id' => 1, 138 | 'question_id' => 5, 139 | 'content' => 'Yes', 140 | 'sort' => 1, 141 | ]); 142 | $this->assertEquals(Answer::findOne(2)->attributes, [ 143 | 'id' => 2, 144 | 'question_id' => 5, 145 | 'content' => 'No', 146 | 'sort' => 2, 147 | ]); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /tests/unit/BasicExporterTest.php: -------------------------------------------------------------------------------- 1 | Test::find()->where(['id' => 1]), 20 | 'filePath' => Yii::getAlias('@tests/_output/BasicExporter.xlsx'), 21 | 'sheetTitle' => 'Tests', 22 | 'standardModelsConfig' => [ 23 | [ 24 | 'className' => Test::className(), 25 | 'attributesOrder' => ['ID', 'Name', 'Type', 'Description', 'Author Name', 'Author Rating'], 26 | 'standardAttributesConfig' => [ 27 | [ 28 | 'name' => 'type', 29 | 'valueReplacement' => function ($model) { 30 | /* @var $model Test */ 31 | return $model->getTypeLabel(); 32 | }, 33 | ], 34 | [ 35 | 'name' => 'description', 36 | 'valueReplacement' => function ($model) { 37 | /* @var $model Test */ 38 | return HtmlPurifier::process($model->description, [ 39 | 'HTML.ForbiddenElements' => ['p'], 40 | ]); 41 | }, 42 | ], 43 | [ 44 | 'name' => 'author_name', 45 | 'label' => 'Author Name', 46 | 'valueReplacement' => function ($model) { 47 | /* @var $model Test */ 48 | return $model->author->name; 49 | }, 50 | ], 51 | [ 52 | 'name' => 'author_rating', 53 | 'label' => 'Author Rating', 54 | 'valueReplacement' => function ($model) { 55 | /* @var $model Test */ 56 | return $model->author->rating; 57 | }, 58 | ], 59 | ], 60 | ], 61 | ], 62 | ]); 63 | 64 | $exporter->run(); 65 | 66 | $phpExcel = PHPExcel_IOFactory::load($exporter->filePath); 67 | 68 | $this->assertEquals($phpExcel->getActiveSheet()->getTitle(), 'Tests'); 69 | 70 | $attributeNames = []; 71 | $attributeValues = []; 72 | $c = 1; 73 | foreach ($phpExcel->getActiveSheet()->getRowIterator() as $row) { 74 | if ($c > 2) { 75 | break; 76 | } 77 | 78 | foreach ($row->getCellIterator() as $cell) { 79 | $column = $cell->getColumn(); 80 | $value = $cell->getValue(); 81 | $c == 1 ? $attributeNames[$column] = $value : $attributeValues[$column] = $value; 82 | } 83 | 84 | $c++; 85 | } 86 | 87 | $attributeValues['A'] = (int) $attributeValues['A']; 88 | $attributeValues['F'] = (int) $attributeValues['F']; 89 | 90 | $this->assertEquals($attributeNames, [ 91 | 'A' => 'ID', 92 | 'B' => 'Name', 93 | 'C' => 'Type', 94 | 'D' => 'Description', 95 | 'E' => 'Author Name', 96 | 'F' => 'Author Rating', 97 | ]); 98 | $this->assertEquals($attributeValues, [ 99 | 'A' => 1, 100 | 'B' => 'Common test', 101 | 'C' => 'Closed', 102 | 'D' => 'This is the common test', 103 | 'E' => 'Ivan Ivanov', 104 | 'F' => 7, 105 | ]); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /tests/unit/BasicImporterTest.php: -------------------------------------------------------------------------------- 1 | $path, 25 | 'standardModelsConfig' => [ 26 | [ 27 | 'className' => Test::className(), 28 | 'standardAttributesConfig' => [ 29 | [ 30 | 'name' => 'type', 31 | 'valueReplacement' => Test::getTypesList(), 32 | ], 33 | [ 34 | 'name' => 'description', 35 | 'valueReplacement' => function ($value) { 36 | return $value ? Html::tag('p', $value) : ''; 37 | }, 38 | ], 39 | [ 40 | 'name' => 'author_id', 41 | 'valueReplacement' => function ($value) { 42 | return Author::find()->select('id')->where(['name' => $value]); 43 | }, 44 | ], 45 | ], 46 | ], 47 | ], 48 | ]); 49 | 50 | $result = $importer->run(); 51 | $this->assertEquals($importer->error, null); 52 | $this->assertEquals($result, true); 53 | 54 | $this->assertEquals(Test::find()->count(), 3); 55 | $this->assertEquals(Test::findOne(1)->attributes, [ 56 | 'id' => 1, 57 | 'name' => 'Basic test', 58 | 'type' => 2, 59 | 'description' => '

This is the basic test

', 60 | 'author_id' => 2, 61 | ]); 62 | $this->assertEquals(Test::findOne(2)->attributes, [ 63 | 'id' => 2, 64 | 'name' => 'Common test', 65 | 'type' => 1, 66 | 'description' => '', 67 | 'author_id' => 1, 68 | ]); 69 | $this->assertEquals(Test::findOne(3)->attributes, [ 70 | 'id' => 3, 71 | 'name' => 'Programming test', 72 | 'type' => 2, 73 | 'description' => '', 74 | 'author_id' => 2, 75 | ]); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /tests/unit/_bootstrap.php: -------------------------------------------------------------------------------- 1 | 'app-console', 4 | 'class' => 'yii\console\Application', 5 | 'basePath' => \Yii::getAlias('@tests'), 6 | 'runtimePath' => \Yii::getAlias('@tests/_output'), 7 | 'bootstrap' => [], 8 | 'components' => [ 9 | 'db' => [ 10 | 'class' => '\yii\db\Connection', 11 | 'dsn' => 'sqlite:' . \Yii::getAlias('@tests/_output/temp.db'), 12 | 'username' => '', 13 | 'password' => '', 14 | ], 15 | ], 16 | ]; 17 | --------------------------------------------------------------------------------