├── tests ├── files │ ├── file │ ├── file.txt │ ├── file.bmp │ ├── file.gif │ ├── file.jpg │ ├── file.mp3 │ ├── file.png │ ├── file.wav │ └── file.zip ├── assets │ └── .gitignore ├── data │ └── db.sqlite ├── views │ ├── test │ │ ├── attachments-input-view.php │ │ ├── attachments-table-view.php │ │ └── attachments-table-view-no-delete.php │ └── layouts │ │ └── main.php ├── FileTest.php ├── bootstrap.php ├── models │ └── Comment.php ├── ModuleTest.php ├── AttachmentsInputTest.php ├── AttachmentsTableTest.php ├── TestCase.php └── FileControllerTest.php ├── .gitignore ├── src ├── messages │ ├── zh-CN │ │ └── attachments.php │ ├── zh-TW │ │ └── attachments.php │ ├── fr │ │ └── attachments.php │ ├── id │ │ └── attachments.php │ ├── it │ │ └── attachments.php │ └── ru │ │ └── attachments.php ├── events │ └── FileEvent.php ├── models │ ├── UploadForm.php │ └── File.php ├── ModuleTrait.php ├── migrations │ └── m150127_040544_add_attachments.php ├── controllers │ └── FileController.php ├── components │ ├── AttachmentsTable.php │ ├── AttachmentsInput.php │ └── AttachmentsTableWithPreview.php ├── Module.php └── behaviors │ └── FileBehavior.php ├── .travis.yml ├── .scrutinizer.yml ├── phpunit.xml.dist ├── composer.json ├── README.md └── composer.lock /tests/files/file: -------------------------------------------------------------------------------- 1 | file 1 -------------------------------------------------------------------------------- /tests/files/file.txt: -------------------------------------------------------------------------------- 1 | file 1 -------------------------------------------------------------------------------- /tests/assets/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /tests/data/db.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CTOlet/yii2-attachments/HEAD/tests/data/db.sqlite -------------------------------------------------------------------------------- /tests/files/file.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CTOlet/yii2-attachments/HEAD/tests/files/file.bmp -------------------------------------------------------------------------------- /tests/files/file.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CTOlet/yii2-attachments/HEAD/tests/files/file.gif -------------------------------------------------------------------------------- /tests/files/file.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CTOlet/yii2-attachments/HEAD/tests/files/file.jpg -------------------------------------------------------------------------------- /tests/files/file.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CTOlet/yii2-attachments/HEAD/tests/files/file.mp3 -------------------------------------------------------------------------------- /tests/files/file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CTOlet/yii2-attachments/HEAD/tests/files/file.png -------------------------------------------------------------------------------- /tests/files/file.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CTOlet/yii2-attachments/HEAD/tests/files/file.wav -------------------------------------------------------------------------------- /tests/files/file.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CTOlet/yii2-attachments/HEAD/tests/files/file.zip -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | .idea/ 3 | build/ 4 | vendor/ 5 | tests/uploads/ 6 | 7 | #use git update-index --assume-unchanged tests/data/db.sqlite 8 | #tests/data/db.sqlite -------------------------------------------------------------------------------- /src/messages/zh-CN/attachments.php: -------------------------------------------------------------------------------- 1 | '模型不能为空。', 8 | 'The behavior FileBehavior has not been attached to the model.' => 'FileBehavior行为尚未附加到模型。', 9 | 'File name' => '文件名' 10 | ]; 11 | -------------------------------------------------------------------------------- /src/messages/zh-TW/attachments.php: -------------------------------------------------------------------------------- 1 | '模型不能為空。', 8 | 'The behavior FileBehavior has not been attached to the model.' => 'FileBehavior行為尚未附加到模型。', 9 | 'File name' => '文件名' 10 | ]; 11 | -------------------------------------------------------------------------------- /tests/views/test/attachments-input-view.php: -------------------------------------------------------------------------------- 1 | $model 15 | ]); 16 | -------------------------------------------------------------------------------- /tests/views/test/attachments-table-view.php: -------------------------------------------------------------------------------- 1 | $model 15 | ]); 16 | -------------------------------------------------------------------------------- /src/messages/fr/attachments.php: -------------------------------------------------------------------------------- 1 | 'Le modèle ne peut pas être vide', 7 | 'The behavior FileBehavior has not been attached to the model.' => 'Le comportement FileBehavior n\'a pas été attaché au modèle.', 8 | 'File name' => 'Nom de fichier' 9 | ]; 10 | -------------------------------------------------------------------------------- /src/messages/id/attachments.php: -------------------------------------------------------------------------------- 1 | 'Model tidak boleh kosong.', 8 | 'The behavior FileBehavior has not been attached to the model.' => 'behavior `FileBehavior` belum dilampirkan ke model.', 9 | 'File name' => 'Nama file' 10 | ]; 11 | -------------------------------------------------------------------------------- /src/messages/it/attachments.php: -------------------------------------------------------------------------------- 1 | 4 | */ 5 | 6 | return [ 7 | 'The model cannot be empty.' => 'Il modello non può essere vuoto.', 8 | 'The behavior FileBehavior has not been attached to the model.' => 'Il behavior FileBehavior non è stato associato al modello.', 9 | 'File name' => 'Nome file' 10 | ]; 11 | -------------------------------------------------------------------------------- /src/messages/ru/attachments.php: -------------------------------------------------------------------------------- 1 | 'Модель не может быть пустой.', 11 | 'The behavior FileBehavior has not been attached to the model.' => 'Поведение FileBehavior не привязано к модели.', 12 | 'File name' => 'Название файла' 13 | ]; -------------------------------------------------------------------------------- /tests/views/test/attachments-table-view-no-delete.php: -------------------------------------------------------------------------------- 1 | $model, 15 | 'showDeleteButton' => false, 16 | ]); 17 | 18 | echo \nemmo\attachments\components\AttachmentsTableWithPreview::widget([ 19 | 'model' => $model, 20 | 'showDeleteButton' => false, 21 | ]); -------------------------------------------------------------------------------- /tests/views/layouts/main.php: -------------------------------------------------------------------------------- 1 | 11 | 12 | beginPage(); ?> 13 | 14 | 15 | 16 | head(); ?> 17 | 18 | 19 | beginBody() ?> 20 | 21 | endBody(); ?> 22 | 23 | 24 | endPage() ?> -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | dist: trusty 4 | 5 | php: 6 | - 5.5 7 | - 5.6 8 | - 7.0 9 | - 7.1 10 | 11 | install: 12 | - travis_retry composer self-update && composer --version 13 | - export PATH="$HOME/.composer/vendor/bin:$PATH" 14 | - COMPOSER_MEMORY_LIMIT=-1 travis_retry composer install --prefer-dist --no-interaction 15 | 16 | script: 17 | - vendor/bin/phpunit --coverage-text --coverage-clover=coverage.clover 18 | 19 | after_script: 20 | - wget https://scrutinizer-ci.com/ocular.phar 21 | - php ocular.phar code-coverage:upload --format=php-clover coverage.clover 22 | -------------------------------------------------------------------------------- /src/events/FileEvent.php: -------------------------------------------------------------------------------- 1 | _files; 20 | } 21 | 22 | /** 23 | * @param nemmo\attachments\models\File[] $files 24 | */ 25 | public function setFiles($files) 26 | { 27 | $this->_files = $files; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/models/UploadForm.php: -------------------------------------------------------------------------------- 1 | getModule()->rules) 32 | ]; 33 | } 34 | } -------------------------------------------------------------------------------- /src/ModuleTrait.php: -------------------------------------------------------------------------------- 1 | _module == null) { 26 | $this->_module = \Yii::$app->getModule('attachments'); 27 | } 28 | 29 | if (!$this->_module) { 30 | throw new \Exception("Yii2 attachment module not found, may be you didn't add it to your config?"); 31 | } 32 | 33 | return $this->_module; 34 | } 35 | } -------------------------------------------------------------------------------- /tests/FileTest.php: -------------------------------------------------------------------------------- 1 | assertFalse($file->validate()); 20 | $this->assertArrayHasKey('name', $file->errors); 21 | $this->assertArrayHasKey('model', $file->errors); 22 | $this->assertArrayHasKey('itemId', $file->errors); 23 | $this->assertArrayHasKey('hash', $file->errors); 24 | $this->assertArrayHasKey('size', $file->errors); 25 | //$this->assertArrayHasKey('type', $file->errors); 26 | $this->assertArrayHasKey('mime', $file->errors); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | ./tests 15 | 16 | 17 | 18 | 19 | ./src/ 20 | 21 | ./src/messages/ 22 | ./src/migrations/ 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nemmo/yii2-attachments", 3 | "description": "Extension for file uploading and attaching to the models", 4 | "type": "yii2-extension", 5 | "keywords": [ 6 | "yii2", 7 | "extension", 8 | "attachment", 9 | "file", 10 | "upload" 11 | ], 12 | "license": "BSD-4-Clause", 13 | "homepage": "https://github.com/Nemmo/yii2-attachments", 14 | "authors": [ 15 | { 16 | "name": "Alimzhan Abuov aka Nemmo", 17 | "email": "alimzhan.abu@yandex.kz", 18 | "homepage": "http://alimzhan.kz", 19 | "role": "Developer" 20 | } 21 | ], 22 | "require": { 23 | "php": ">=5.4.0", 24 | "yiisoft/yii2": "~2.0.0", 25 | "kartik-v/yii2-widget-fileinput": "~1.1.1", 26 | "himiklab/yii2-colorbox-widget": "*" 27 | }, 28 | "require-dev": { 29 | "phpunit/phpunit": "~4.0", 30 | "phpunit/dbunit": "~1.0" 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "nemmo\\attachments\\": "src" 35 | } 36 | }, 37 | "extra": { 38 | "asset-installer-paths": { 39 | "npm-asset-library": "vendor/npm", 40 | "bower-asset-library": "vendor/bower" 41 | } 42 | }, 43 | "repositories": [ 44 | { 45 | "type": "composer", 46 | "url": "https://asset-packagist.org" 47 | } 48 | ], 49 | "config": { 50 | "process-timeout": 1800, 51 | "fxp-asset": { 52 | "enabled": false 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/migrations/m150127_040544_add_attachments.php: -------------------------------------------------------------------------------- 1 | db->driverName === 'mysql') { 16 | // http://stackoverflow.com/questions/766809/whats-the-difference-between-utf8-general-ci-and-utf8-unicode-ci 17 | $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE=InnoDB'; 18 | } 19 | 20 | $this->createTable($this->getModule()->tableName, [ 21 | 'id' => Schema::TYPE_PK, 22 | 'name' => Schema::TYPE_STRING . ' not null', 23 | 'model' => Schema::TYPE_STRING . ' not null', 24 | 'itemId' => Schema::TYPE_INTEGER . ' not null', 25 | 'hash' => Schema::TYPE_STRING . ' not null', 26 | 'size' => Schema::TYPE_INTEGER . ' not null', 27 | 'type' => Schema::TYPE_STRING . ' not null', 28 | 'mime' => Schema::TYPE_STRING . ' not null' 29 | ], $tableOptions); 30 | 31 | $this->createIndex('file_model', $this->getModule()->tableName, 'model'); 32 | $this->createIndex('file_item_id', $this->getModule()->tableName, 'itemId'); 33 | } 34 | 35 | public function down() 36 | { 37 | $this->dropTable($this->getModule()->tableName); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/ModuleTest.php: -------------------------------------------------------------------------------- 1 | _module = Yii::$app->getModule('attachments'); 23 | } 24 | 25 | protected function tearDown() 26 | { 27 | Yii::$app->setModule('attachments', $this->_module); 28 | 29 | parent::tearDown(); 30 | } 31 | 32 | public function testInitException() 33 | { 34 | Yii::$app->setModule('attachments', [ 35 | 'class' => Module::className(), 36 | 'storePath' => '' 37 | ]); 38 | $this->setExpectedException('Exception', 'Setup {storePath} and {tempPath} in module properties'); 39 | Yii::$app->getModule('attachments'); 40 | } 41 | 42 | public function testInit() 43 | { 44 | Yii::$app->setModule('attachments', [ 45 | 'class' => Module::className() 46 | ]); 47 | /** @var Module $module */ 48 | $module = Yii::$app->getModule('attachments'); 49 | $this->assertEquals([ 50 | 'maxFiles' => 3 51 | ], $module->rules); 52 | 53 | $newRules = [ 54 | 'maxFiles' => 10, 55 | 'mimeTypes' => 'image/png', 56 | 'maxSize' => 1024 57 | ]; 58 | Yii::$app->setModule('attachments', [ 59 | 'class' => Module::className(), 60 | 'rules' => $newRules 61 | ]); 62 | $module = Yii::$app->getModule('attachments'); 63 | $this->assertEquals($newRules, $module->rules); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/models/File.php: -------------------------------------------------------------------------------- 1 | getModule('attachments')->tableName; 32 | } 33 | 34 | /** 35 | * @inheritDoc 36 | */ 37 | public function fields() 38 | { 39 | return [ 40 | 'url' 41 | ]; 42 | } 43 | 44 | /** 45 | * @inheritdoc 46 | */ 47 | public function rules() 48 | { 49 | return [ 50 | [['name', 'model', 'itemId', 'hash', 'size', 'mime'], 'required'], 51 | [['itemId', 'size'], 'integer'], 52 | [['name', 'model', 'hash', 'type', 'mime'], 'string', 'max' => 255] 53 | ]; 54 | } 55 | 56 | /** 57 | * @inheritdoc 58 | */ 59 | public function attributeLabels() 60 | { 61 | return [ 62 | 'id' => 'ID', 63 | 'name' => 'Name', 64 | 'model' => 'Model', 65 | 'itemId' => 'Item ID', 66 | 'hash' => 'Hash', 67 | 'size' => 'Size', 68 | 'type' => 'Type', 69 | 'mime' => 'Mime' 70 | ]; 71 | } 72 | 73 | public function getUrl($inline = false) 74 | { 75 | return Url::to(['/attachments/file/download', 'id' => $this->id,'inline'=>$inline]); 76 | } 77 | 78 | public function getPath() 79 | { 80 | return $this->getModule()->getFilesDirPath($this->hash) . DIRECTORY_SEPARATOR . $this->hash . '.' . $this->type; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /tests/AttachmentsInputTest.php: -------------------------------------------------------------------------------- 1 | setExpectedException('\yii\base\InvalidConfigException'); 21 | AttachmentsInput::widget(); 22 | } 23 | 24 | public function testDefaultConfig() 25 | { 26 | Yii::$app->controller = new Controller('test', Yii::$app); 27 | $response = Yii::$app->controller->render('attachments-input-view', [ 28 | 'model' => new Comment() 29 | ]); 30 | 31 | $this->assertContains("var fileInput = $('#file-input');", $response); 32 | $this->assertContains("UploadForm[file][]", $response); 33 | $this->assertContains('jquery.js', $response); 34 | $this->assertContains('fileinput.js', $response); 35 | $this->assertContains('fileinput.css', $response); 36 | $this->assertContains('kv-widgets.css', $response); 37 | } 38 | 39 | public function testDefaultConfigOldModel() 40 | { 41 | $comment = new Comment(); 42 | $comment->text = 'test'; 43 | $this->generateFiles(['png', 'jpg', 'txt']); 44 | $comment->save(); 45 | 46 | Yii::$app->controller = new Controller('test', Yii::$app); 47 | $response = Yii::$app->controller->render('attachments-input-view', [ 48 | 'model' => $comment 49 | ]); 50 | 51 | $this->assertContains("var fileInput = $('#file-input');", $response); 52 | $this->assertContains("UploadForm[file][]", $response); 53 | $this->assertContains('jquery.js', $response); 54 | $this->assertContains('fileinput.js', $response); 55 | $this->assertContains('fileinput.css', $response); 56 | $this->assertContains('kv-widgets.css', $response); 57 | $this->assertContains('file-preview-image', $response); 58 | $this->assertContains('file-preview-other', $response); 59 | $this->assertContains('attachments%2Ffile%2Fdelete', $response); 60 | } 61 | } -------------------------------------------------------------------------------- /tests/AttachmentsTableTest.php: -------------------------------------------------------------------------------- 1 | setExpectedException('\yii\base\InvalidConfigException'); 22 | AttachmentsTable::widget(); 23 | } 24 | 25 | public function testEmptyBehaviorConfig() 26 | { 27 | $this->setExpectedException('\yii\base\InvalidConfigException'); 28 | $model = new ActiveRecord(); 29 | AttachmentsTable::widget(['model' => $model]); 30 | } 31 | 32 | public function testDefaultConfig() 33 | { 34 | $comment = new Comment(); 35 | $comment->text = 'test'; 36 | 37 | $types = ['png', 'txt', 'jpg']; 38 | $this->generateFiles($types); 39 | Yii::$app->runAction('attachments/file/upload'); 40 | 41 | $comment->save(); 42 | 43 | Yii::$app->controller = new Controller('test', Yii::$app, ['action' => 'test']); 44 | $response = Yii::$app->controller->render('attachments-table-view', [ 45 | 'model' => $comment 46 | ]); 47 | 48 | $this->assertContains('table table-striped table-bordered table-condensed', $response); 49 | $this->assertContains("yii.gridView.js", $response); 50 | $this->assertContains('jquery.js', $response); 51 | $this->assertContains('yii.js', $response); 52 | $this->assertContains('yiiGridView', $response); 53 | $this->assertContains('file.png', $response); 54 | $this->assertContains('file.txt', $response); 55 | $this->assertContains('file.jpg', $response); 56 | $this->assertContains('text = 'test'; 63 | 64 | $types = ['png', 'txt', 'jpg']; 65 | $this->generateFiles($types); 66 | Yii::$app->runAction('attachments/file/upload'); 67 | 68 | $comment->save(); 69 | 70 | Yii::$app->controller = new Controller('test', Yii::$app, ['action' => 'test']); 71 | $response = Yii::$app->controller->render('attachments-table-view-no-delete', [ 72 | 'model' => $comment 73 | ]); 74 | 75 | $this->assertNotContains('file = UploadedFile::getInstances($model, 'file'); 22 | 23 | if ($model->rules()[0]['maxFiles'] == 1 && sizeof($model->file) == 1) { 24 | $model->file = $model->file[0]; 25 | } 26 | 27 | if ($model->file && $model->validate()) { 28 | $result['uploadedFiles'] = []; 29 | if (is_array($model->file)) { 30 | foreach ($model->file as $file) { 31 | $path = $this->getModule()->getUserDirPath() . DIRECTORY_SEPARATOR . $file->name; 32 | $file->saveAs($path); 33 | $result['uploadedFiles'][] = $file->name; 34 | } 35 | } else { 36 | $path = $this->getModule()->getUserDirPath() . DIRECTORY_SEPARATOR . $model->file->name; 37 | $model->file->saveAs($path); 38 | $result['uploadedFiles'][] = $model->file->name; 39 | } 40 | Yii::$app->response->format = Response::FORMAT_JSON; 41 | return $result; 42 | } else { 43 | Yii::$app->response->format = Response::FORMAT_JSON; 44 | return [ 45 | 'error' => $model->getErrors('file') 46 | ]; 47 | } 48 | } 49 | 50 | public function actionDownload($id,$inline=false) 51 | { 52 | $file = File::findOne(['id' => $id]); 53 | $filePath = $this->getModule()->getFilesDirPath($file->hash) . DIRECTORY_SEPARATOR . $file->hash . '.' . $file->type; 54 | 55 | return Yii::$app->response->sendFile($filePath, "$file->name.$file->type",['inline'=>$inline]); 56 | } 57 | 58 | public function actionDelete($id) 59 | { 60 | if ($this->getModule()->detachFile($id)) { 61 | return true; 62 | } else { 63 | return false; 64 | } 65 | } 66 | 67 | public function actionDownloadTemp($filename) 68 | { 69 | $filePath = $this->getModule()->getUserDirPath() . DIRECTORY_SEPARATOR . $filename; 70 | 71 | return Yii::$app->response->sendFile($filePath, $filename); 72 | } 73 | 74 | public function actionDeleteTemp($filename) 75 | { 76 | $userTempDir = $this->getModule()->getUserDirPath(); 77 | $filePath = $userTempDir . DIRECTORY_SEPARATOR . $filename; 78 | unlink($filePath); 79 | if (!sizeof(FileHelper::findFiles($userTempDir))) { 80 | rmdir($userTempDir); 81 | } 82 | 83 | Yii::$app->response->format = Response::FORMAT_JSON; 84 | return []; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/components/AttachmentsTable.php: -------------------------------------------------------------------------------- 1 | 'table table-striped table-bordered table-condensed']; 30 | 31 | public $showDeleteButton = true; 32 | 33 | public function init() 34 | { 35 | parent::init(); 36 | 37 | if (empty($this->model)) { 38 | throw new InvalidConfigException("Property {model} cannot be blank"); 39 | } 40 | 41 | $hasFileBehavior = false; 42 | foreach ($this->model->getBehaviors() as $behavior) { 43 | if (is_a($behavior, FileBehavior::className())) { 44 | $hasFileBehavior = true; 45 | } 46 | } 47 | if (!$hasFileBehavior) { 48 | throw new InvalidConfigException("The behavior {FileBehavior} has not been attached to the model."); 49 | } 50 | } 51 | 52 | public function run() 53 | { 54 | $confirm = Yii::t('yii', 'Are you sure you want to delete this item?'); 55 | $js = <<view->registerJs($js); 73 | 74 | return GridView::widget([ 75 | 'dataProvider' => new ArrayDataProvider(['allModels' => $this->model->getFiles()]), 76 | 'layout' => '{items}', 77 | 'tableOptions' => $this->tableOptions, 78 | 'columns' => [ 79 | [ 80 | 'class' => 'yii\grid\SerialColumn' 81 | ], 82 | [ 83 | 'label' => $this->getModule()->t('attachments', 'File name'), 84 | 'format' => 'raw', 85 | 'value' => function ($model) { 86 | return Html::a("$model->name.$model->type", $model->getUrl()); 87 | } 88 | ], 89 | [ 90 | 'class' => 'yii\grid\ActionColumn', 91 | 'template' => '{delete}', 92 | 'visibleButtons' => ['delete' => $this->showDeleteButton], 93 | 'buttons' => [ 94 | 'delete' => function ($url, $model, $key) { 95 | return Html::a('', 96 | '#', 97 | [ 98 | 'class' => 'delete-button', 99 | 'title' => Yii::t('yii', 'Delete'), 100 | 'data-url' => Url::to(['/attachments/file/delete', 'id' => $model->id]) 101 | ] 102 | ); 103 | } 104 | ] 105 | ], 106 | ] 107 | ]); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/components/AttachmentsInput.php: -------------------------------------------------------------------------------- 1 | model)) { 37 | throw new InvalidConfigException("Property {model} cannot be blank"); 38 | } 39 | 40 | FileHelper::removeDirectory($this->getModule()->getUserDirPath()); // Delete all uploaded files in past 41 | 42 | $this->pluginOptions = array_replace($this->pluginOptions, [ 43 | 'initialPreviewFileType'=> 'image', 44 | 'initialPreviewAsData'=> 'true', //let kartik deal with the previews! 45 | 'uploadUrl' => Url::toRoute('/attachments/file/upload'), 46 | 'uploadAsync' => false 47 | ]); 48 | if(!isset($this->pluginOptions['initialPreview'])){ 49 | $this->pluginOptions = array_replace($this->pluginOptions, [ 50 | 'initialPreview' => $this->model->isNewRecord ? [] : $this->model->getInitialPreview(), 51 | ]); 52 | } 53 | if(!isset($this->pluginOptions['initialPreviewConfig'])){ 54 | $this->pluginOptions = array_replace($this->pluginOptions, [ 55 | 'initialPreviewConfig' => $this->model->isNewRecord ? [] : $this->model->getInitialPreviewConfig(), 56 | ]); 57 | } 58 | 59 | $this->options = array_replace($this->options, [ 60 | 'id' => $this->id, 61 | //'multiple' => true 62 | ]); 63 | 64 | $js = <<view->registerJs($js); 111 | } 112 | 113 | public function run() 114 | { 115 | $fileinput = FileInput::widget([ 116 | 'model' => new UploadForm(), 117 | 'attribute' => 'file[]', 118 | 'options' => $this->options, 119 | 'pluginOptions' => $this->pluginOptions 120 | ]); 121 | 122 | return Html::tag('div', $fileinput, ['class' => 'form-group']); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | mockApplication(); 30 | Yii::$app->db->createCommand()->truncateTable('attach_file')->execute(); 31 | Yii::$app->db->createCommand()->truncateTable('comment')->execute(); 32 | Yii::$app->db->createCommand()->truncateTable('sqlite_sequence')->execute(); 33 | } 34 | 35 | /** 36 | * @inheritdoc 37 | */ 38 | protected function tearDown() 39 | { 40 | $this->destroyApplication(); 41 | } 42 | 43 | protected function mockApplication($config = [], $appClass = '\yii\web\Application') 44 | { 45 | new $appClass(ArrayHelper::merge([ 46 | 'id' => 'test-app', 47 | 'basePath' => Yii::getAlias('@tests'), 48 | 'vendorPath' => Yii::getAlias('@tests/../vendor'), 49 | 'aliases' => [ 50 | '@bower' => '@vendor/bower-asset', 51 | '@npm' => '@vendor/npm-asset', 52 | ], 53 | 'modules' => [ 54 | 'attachments' => [ 55 | 'class' => \nemmo\attachments\Module::className(), 56 | ] 57 | ], 58 | 'components' => [ 59 | // 'urlManager' => [ 60 | // 'class' => \yii\web\UrlManager::className(), 61 | // 'baseUrl' => 'http://localhost', 62 | // 'scriptUrl' => '/index.php' 63 | // ], 64 | 'request' => [ 65 | 'cookieValidationKey' => 'wefJDF8sfdsfSDefwqdxj9oq', 66 | 'scriptFile' => Yii::getAlias('@tests/index.php'), 67 | 'scriptUrl' => '/index.php', 68 | ], 69 | 'db' => [ 70 | 'class' => \yii\db\Connection::className(), 71 | 'dsn' => 'sqlite:' . Yii::getAlias('@tests/data/db.sqlite') 72 | ], 73 | 'assetManager' => [ 74 | 'basePath' => '@tests/assets', 75 | 'baseUrl' => '/', 76 | ] 77 | ] 78 | ], $config)); 79 | } 80 | 81 | protected function destroyApplication() 82 | { 83 | Yii::$app = null; 84 | } 85 | 86 | /** 87 | * Creates a view for testing purposes 88 | * 89 | * @return View 90 | */ 91 | protected function getView() 92 | { 93 | $view = new View(); 94 | $view->setAssetManager(new AssetManager([ 95 | 'basePath' => '@tests/assets', 96 | 'baseUrl' => '/', 97 | ])); 98 | return $view; 99 | } 100 | 101 | /** 102 | * Asserting two strings equality ignoring line endings 103 | * 104 | * @param string $expected 105 | * @param string $actual 106 | */ 107 | public function assertEqualsWithoutLE($expected, $actual) 108 | { 109 | $expected = str_replace("\r\n", "\n", $expected); 110 | $actual = str_replace("\r\n", "\n", $actual); 111 | $this->assertEquals($expected, $actual); 112 | } 113 | 114 | public function generateFiles($types) 115 | { 116 | $_FILES = []; 117 | UploadedFile::reset(); 118 | 119 | foreach ($types as $index => $type) { 120 | $file = $type ? "file.$type" : "file"; 121 | $path = Yii::getAlias("@tests/files/$file"); 122 | $_FILES["UploadForm[file][$index]"] = [ 123 | 'name' => $file, 124 | 'type' => mime_content_type($path), 125 | 'size' => filesize($path), 126 | 'tmp_name' => $path, 127 | 'error' => 0 128 | ]; 129 | } 130 | } 131 | 132 | public function checkFilesExist($types) 133 | { 134 | foreach ($types as $type) { 135 | $file = $type ? "/file.$type" : "/file"; 136 | $filePath = $this->getTempDirPath() . $file; 137 | $this->assertFileExists($filePath); 138 | } 139 | } 140 | 141 | public function checkFilesNotExist($types) 142 | { 143 | foreach ($types as $type) { 144 | $file = $type ? "/file.$type" : "/file"; 145 | $filePath = $this->getTempDirPath() . $file; 146 | $this->assertFileNotExists($filePath); 147 | } 148 | } 149 | 150 | public function getTempDirPath() 151 | { 152 | return Yii::getAlias('@tests/uploads/temp/' . Yii::$app->session->id); 153 | } 154 | } -------------------------------------------------------------------------------- /src/components/AttachmentsTableWithPreview.php: -------------------------------------------------------------------------------- 1 | 'table table-striped table-bordered table-condensed']; 30 | 31 | public $showDeleteButton = true; 32 | 33 | public function init() 34 | { 35 | parent::init(); 36 | } 37 | 38 | public function run() 39 | { 40 | if (!$this->model) { 41 | return Html::tag('div', 42 | Html::tag('b', 43 | Yii::t('yii', 'Error')) . ': ' . $this->getModule()->t('attachments', 'The model cannot be empty.' 44 | ), 45 | [ 46 | 'class' => 'alert alert-danger' 47 | ] 48 | ); 49 | } 50 | 51 | $hasFileBehavior = false; 52 | foreach ($this->model->getBehaviors() as $behavior) { 53 | if ($behavior instanceof FileBehavior) { 54 | $hasFileBehavior = true; 55 | break; 56 | } 57 | } 58 | if (!$hasFileBehavior) { 59 | return Html::tag('div', 60 | Html::tag('b', 61 | Yii::t('yii', 'Error')) . ': ' . $this->getModule()->t('attachments', 'The behavior FileBehavior has not been attached to the model.' 62 | ), 63 | [ 64 | 'class' => 'alert alert-danger' 65 | ] 66 | ); 67 | } 68 | 69 | $confirm = Yii::t('yii', 'Are you sure you want to delete this item?'); 70 | $js = <<view->registerJs($js); 88 | 89 | return GridView::widget([ 90 | 'dataProvider' => new ArrayDataProvider(['allModels' => $this->model->getFiles()]), 91 | 'layout' => '{items}', 92 | 'tableOptions' => $this->tableOptions, 93 | 'columns' => [ 94 | [ 95 | 'class' => 'yii\grid\SerialColumn' 96 | ], 97 | [ 98 | 'label' => $this->getModule()->t('attachments', 'File name'), 99 | 'format' => 'raw', 100 | 'value' => function ($model) { 101 | return Html::a("$model->name.$model->type", $model->getUrl(), [ 102 | 'class' => ' group' . $model->itemId, 103 | 'onclick' => 'return false;', 104 | ]); 105 | } 106 | ], 107 | [ 108 | 'class' => 'yii\grid\ActionColumn', 109 | 'template' => '{delete}', 110 | 'visibleButtons' => ['delete' => $this->showDeleteButton], 111 | 'buttons' => [ 112 | 'delete' => function ($url, $model, $key) { 113 | return Html::a('', 114 | '#', 115 | [ 116 | 'class' => 'delete-button', 117 | 'title' => Yii::t('yii', 'Delete'), 118 | 'data-url' => Url::to(['/attachments/file/delete', 'id' => $model->id]) 119 | ] 120 | ); 121 | } 122 | ] 123 | ], 124 | ], 125 | ]) . 126 | Colorbox::widget([ 127 | 'targets' => [ 128 | '.group' . $this->model->id => [ 129 | 'rel' => '.group' . $this->model->id, 130 | 'photo' => true, 131 | 'scalePhotos' => true, 132 | 'width' => '100%', 133 | 'height' => '100%', 134 | 'maxWidth' => 800, 135 | 'maxHeight' => 600, 136 | ], 137 | ], 138 | 'coreStyle' => 4, 139 | 140 | ]); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/Module.php: -------------------------------------------------------------------------------- 1 | storePath) || empty($this->tempPath)) { 28 | throw new Exception('Setup {storePath} and {tempPath} in module properties'); 29 | } 30 | 31 | $this->rules = ArrayHelper::merge(['maxFiles' => 3], $this->rules); 32 | $this->defaultRoute = 'file'; 33 | $this->registerTranslations(); 34 | } 35 | 36 | public function registerTranslations() 37 | { 38 | \Yii::$app->i18n->translations['nemmo/*'] = [ 39 | 'class' => PhpMessageSource::className(), 40 | 'sourceLanguage' => 'en', 41 | 'basePath' => '@vendor/nemmo/yii2-attachments/src/messages', 42 | 'fileMap' => [ 43 | 'nemmo/attachments' => 'attachments.php' 44 | ], 45 | ]; 46 | } 47 | 48 | public static function t($category, $message, $params = [], $language = null) 49 | { 50 | return \Yii::t('nemmo/' . $category, $message, $params, $language); 51 | } 52 | 53 | public function getStorePath() 54 | { 55 | return \Yii::getAlias($this->storePath); 56 | } 57 | 58 | public function getTempPath() 59 | { 60 | return \Yii::getAlias($this->tempPath); 61 | } 62 | 63 | /** 64 | * @param $fileHash 65 | * @return string 66 | */ 67 | public function getFilesDirPath($fileHash) 68 | { 69 | $path = $this->getStorePath() . DIRECTORY_SEPARATOR . $this->getSubDirs($fileHash); 70 | 71 | FileHelper::createDirectory($path); 72 | 73 | return $path; 74 | } 75 | 76 | public function getSubDirs($fileHash, $depth = 3) 77 | { 78 | $depth = min($depth, 9); 79 | $path = ''; 80 | 81 | for ($i = 0; $i < $depth; $i++) { 82 | $folder = substr($fileHash, $i * 3, 2); 83 | $path .= $folder; 84 | if ($i != $depth - 1) $path .= DIRECTORY_SEPARATOR; 85 | } 86 | 87 | return $path; 88 | } 89 | 90 | public function getUserDirPath() 91 | { 92 | \Yii::$app->session->open(); 93 | 94 | $userDirPath = $this->getTempPath() . DIRECTORY_SEPARATOR . \Yii::$app->session->id; 95 | FileHelper::createDirectory($userDirPath); 96 | 97 | \Yii::$app->session->close(); 98 | 99 | return $userDirPath . DIRECTORY_SEPARATOR; 100 | } 101 | 102 | public function getShortClass($obj) 103 | { 104 | $className = get_class($obj); 105 | if (preg_match('@\\\\([\w]+)$@', $className, $matches)) { 106 | $className = $matches[1]; 107 | } 108 | return $className; 109 | } 110 | 111 | /** 112 | * @param $filePath string 113 | * @param $owner 114 | * @return bool|File 115 | * @throws Exception 116 | * @throws \yii\base\InvalidConfigException 117 | */ 118 | public function attachFile($filePath, $owner) 119 | { 120 | if (empty($owner->id)) { 121 | throw new Exception('Parent model must have ID when you attaching a file'); 122 | } 123 | if (!file_exists($filePath)) { 124 | throw new Exception("File $filePath not exists"); 125 | } 126 | 127 | $fileHash = md5(microtime(true) . $filePath); 128 | $fileType = pathinfo($filePath, PATHINFO_EXTENSION); 129 | $newFileName = "$fileHash.$fileType"; 130 | $fileDirPath = $this->getFilesDirPath($fileHash); 131 | $newFilePath = $fileDirPath . DIRECTORY_SEPARATOR . $newFileName; 132 | 133 | if (!copy($filePath, $newFilePath)) { 134 | throw new Exception("Cannot copy file! $filePath to $newFilePath"); 135 | } 136 | 137 | $file = new File(); 138 | $file->name = pathinfo($filePath, PATHINFO_FILENAME); 139 | $file->model = $this->getShortClass($owner); 140 | $file->itemId = $owner->id; 141 | $file->hash = $fileHash; 142 | $file->size = filesize($filePath); 143 | $file->type = $fileType; 144 | $file->mime = FileHelper::getMimeType($filePath); 145 | 146 | if ($file->save()) { 147 | unlink($filePath); 148 | return $file; 149 | } else { 150 | return false; 151 | } 152 | } 153 | 154 | public function detachFile($id) 155 | { 156 | /** @var File $file */ 157 | $file = File::findOne(['id' => $id]); 158 | if (empty($file)) return false; 159 | $filePath = $this->getFilesDirPath($file->hash) . DIRECTORY_SEPARATOR . $file->hash . '.' . $file->type; 160 | 161 | // this is the important part of the override. 162 | // the original methods doesn't check for file_exists to be 163 | return file_exists($filePath) ? unlink($filePath) && $file->delete() : $file->delete(); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/behaviors/FileBehavior.php: -------------------------------------------------------------------------------- 1 | 'saveUploads', 31 | ActiveRecord::EVENT_AFTER_UPDATE => 'saveUploads', 32 | ActiveRecord::EVENT_AFTER_DELETE => 'deleteUploads' 33 | ]; 34 | } 35 | 36 | public function saveUploads($event) 37 | { 38 | $files = UploadedFile::getInstancesByName('UploadForm[file]'); 39 | 40 | if (!empty($files)) { 41 | foreach ($files as $file) { 42 | if (!$file->saveAs($this->getModule()->getUserDirPath() . $file->name)) { 43 | throw new \Exception(\Yii::t('yii', 'File upload failed.')); 44 | } 45 | } 46 | } 47 | 48 | $userTempDir = $this->getModule()->getUserDirPath(); 49 | $attachedFiles = []; 50 | foreach (FileHelper::findFiles($userTempDir) as $file) { 51 | if (!($attachedFile = $this->getModule()->attachFile($file, $this->owner))) { 52 | throw new \Exception(\Yii::t('yii', 'File upload failed.')); 53 | }else{ 54 | $attachedFiles[] = $attachedFile; 55 | } 56 | } 57 | if(count($attachedFiles)){ 58 | $event = \Yii::createObject(['class' => FileEvent::class, 'files' => $attachedFiles]); 59 | $this->owner->trigger(self::EVENT_AFTER_ATTACH_FILES, $event); 60 | } 61 | rmdir($userTempDir); 62 | } 63 | 64 | public function deleteUploads($event) 65 | { 66 | foreach ($this->getFiles() as $file) { 67 | $this->getModule()->detachFile($file->id); 68 | } 69 | } 70 | 71 | /** 72 | * @return File[] 73 | * @throws \Exception 74 | */ 75 | public function getFiles() 76 | { 77 | $fileQuery = File::find() 78 | ->where([ 79 | 'itemId' => $this->owner->id, 80 | 'model' => $this->getModule()->getShortClass($this->owner) 81 | ]); 82 | $fileQuery->orderBy(['id' => SORT_ASC]); 83 | 84 | return $fileQuery->all(); 85 | } 86 | 87 | public function getInitialPreview() 88 | { 89 | $initialPreview = []; 90 | 91 | $userTempDir = $this->getModule()->getUserDirPath(); 92 | foreach (FileHelper::findFiles($userTempDir) as $file) { 93 | if (substr(FileHelper::getMimeType($file), 0, 5) === 'image') { 94 | $initialPreview[] = Html::img(['/attachments/file/download-temp', 'filename' => basename($file)], ['class' => 'file-preview-image']); 95 | } else { 96 | $initialPreview[] = Html::beginTag('div', ['class' => 'file-preview-other']) . 97 | Html::beginTag('h2') . 98 | Html::tag('i', '', ['class' => 'glyphicon glyphicon-file']) . 99 | Html::endTag('h2') . 100 | Html::endTag('div'); 101 | } 102 | } 103 | 104 | foreach ($this->getFiles() as $file) { 105 | $initialPreview[] =Url::to([$file->getUrl(true)],true); 106 | } 107 | 108 | return $initialPreview; 109 | } 110 | 111 | public function getInitialPreviewConfig() 112 | { 113 | $initialPreviewConfig = []; 114 | 115 | $userTempDir = $this->getModule()->getUserDirPath(); 116 | foreach (FileHelper::findFiles($userTempDir) as $file) { 117 | $filename = basename($file); 118 | $initialPreviewConfig[] = [ 119 | 'caption' => $filename, 120 | 'url' => Url::to(['/attachments/file/delete-temp', 121 | 'filename' => $filename 122 | ]), 123 | ]; 124 | } 125 | 126 | foreach ($this->getFiles() as $index => $file) { 127 | if(str_contains($file->mime,"image")) 128 | { 129 | $initialPreviewConfig[] = [ 130 | 'caption' => "$file->name.$file->type", 131 | 'url' => Url::toRoute(['/attachments/file/delete', 132 | 'id' => $file->id]) 133 | ]; 134 | } 135 | elseif(str_contains($file->mime,"text")){ 136 | $initialPreviewConfig[] = [ 137 | 'type'=> "text", 138 | 'caption' => "$file->name.$file->type", 139 | 'url' => Url::toRoute(['/attachments/file/delete', 140 | 'id' => $file->id]) 141 | ]; 142 | } 143 | elseif(str_contains($file->mime,"video")){ 144 | $initialPreviewConfig[] = [ 145 | 'type'=> "video", 146 | 'caption' => "$file->name.$file->type", 147 | 'url' => Url::toRoute(['/attachments/file/delete', 148 | 'id' => $file->id]) 149 | ]; 150 | } 151 | elseif(str_contains($file->type,"doc")||str_contains($file->type,"ppt")||str_contains($file->type,"xls")){ 152 | $initialPreviewConfig[] = [ 153 | 'type'=> "office", 154 | 'caption' => "$file->name.$file->type", 155 | 'url' => Url::toRoute(['/attachments/file/delete', 156 | 'id' => $file->id]) 157 | ]; 158 | } 159 | else{ 160 | $initialPreviewConfig[] = [ 161 | 'type'=> $file->type, 162 | 'caption' => "$file->name.$file->type", 163 | 'url' => Url::toRoute(['/attachments/file/delete', 164 | 'id' => $file->id]) 165 | ]; 166 | } 167 | } 168 | return $initialPreviewConfig; 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /tests/FileControllerTest.php: -------------------------------------------------------------------------------- 1 | generateFiles($types); 25 | $response = Yii::$app->runAction('attachments/file/upload'); 26 | $this->assertArrayHasKey('uploadedFiles', $response); 27 | $this->assertTrue(in_array('file.png', $response['uploadedFiles'])); 28 | $this->checkFilesExist($types); 29 | foreach ($types as $type) { 30 | /** @var Response $response */ 31 | $response = Yii::$app->runAction('attachments/file/download-temp', ['filename' => "file.$type"]); 32 | ob_start(); 33 | $response->send(); 34 | $actual = ob_get_clean(); 35 | $response->clear(); 36 | $expected = file_get_contents(Yii::getAlias("@tests/files/file.$type")); 37 | $this->assertEquals($expected, $actual); 38 | 39 | $response = Yii::$app->runAction('attachments/file/delete-temp', ['filename' => "file.$type"]); 40 | $this->assertEquals($response, []); 41 | } 42 | $this->checkFilesNotExist($types); 43 | $this->assertFileNotExists($this->getTempDirPath()); 44 | 45 | $comment = new Comment(); 46 | $comment->text = 'test'; 47 | $this->assertTrue($comment->save()); 48 | 49 | $file = $comment->files[0]; 50 | /** @var Response $response */ 51 | $response = Yii::$app->runAction('attachments/file/download', ['id' => $file->id]); 52 | ob_start(); 53 | $response->send(); 54 | $actual = ob_get_clean(); 55 | $response->clear(); 56 | $expected = file_get_contents(Yii::getAlias("@tests/files/file.{$file->type}")); 57 | $this->assertEquals($expected, $actual); 58 | $this->assertFileExists($file->path); 59 | $response = Yii::$app->runAction('attachments/file/delete', ['id' => -1]); 60 | $this->assertEquals($response, false); 61 | $response = Yii::$app->runAction('attachments/file/delete', ['id' => $file->id]); 62 | $this->assertEquals($response, true); 63 | $this->assertFileNotExists($file->path); 64 | 65 | $this->assertNotSame(false, $comment->delete()); 66 | } 67 | 68 | public function testPreUpload2() 69 | { 70 | $types = ['png', 'txt', 'jpg', 'zip']; 71 | $this->generateFiles($types); 72 | $response = Yii::$app->runAction('attachments/file/upload'); 73 | $this->assertArrayHasKey('error', $response); 74 | $errorMessage = 'You can upload at most 3 files.'; 75 | $this->assertTrue(in_array($errorMessage, $response['error'])); 76 | } 77 | 78 | public function testPreUpload3() 79 | { 80 | Yii::$app->setModule('attachments', [ 81 | 'class' => Module::className(), 82 | 'rules' => [ 83 | 'maxFiles' => 1 84 | ] 85 | ]); 86 | $types = ['png', 'zip']; 87 | $this->generateFiles($types); 88 | $response = Yii::$app->runAction('attachments/file/upload'); 89 | $this->assertArrayHasKey('error', $response); 90 | $errorMessage = 'Please upload a file.'; 91 | $this->assertTrue(in_array($errorMessage, $response['error'])); 92 | } 93 | 94 | public function testPreUpload4() 95 | { 96 | Yii::$app->setModule('attachments', [ 97 | 'class' => Module::className(), 98 | 'rules' => [ 99 | 'maxFiles' => 1 100 | ] 101 | ]); 102 | $types = ['png']; 103 | $this->generateFiles($types); 104 | $response = Yii::$app->runAction('attachments/file/upload'); 105 | $this->assertArrayHasKey('uploadedFiles', $response); 106 | $this->assertTrue(in_array('file.png', $response['uploadedFiles'])); 107 | $this->checkFilesExist($types); 108 | } 109 | 110 | public function testPreUpload5() 111 | { 112 | Yii::$app->setModule('attachments', [ 113 | 'class' => Module::className(), 114 | 'rules' => [ 115 | 'maxFiles' => 3, 116 | 'mimeTypes' => ['image/png', 'image/jpeg'] 117 | ] 118 | ]); 119 | $types = ['png', 'jpg', 'zip']; 120 | $this->generateFiles($types); 121 | $response = Yii::$app->runAction('attachments/file/upload'); 122 | $this->assertArrayHasKey('error', $response); 123 | $errorMessage = 'Only files with these MIME types are allowed: image/png, image/jpeg.'; 124 | $this->assertTrue(in_array($errorMessage, $response['error'])); 125 | } 126 | 127 | public function testPreUpload6() 128 | { 129 | $types = ['png', '']; 130 | $this->generateFiles($types); 131 | $response = Yii::$app->runAction('attachments/file/upload'); 132 | $this->assertArrayHasKey('uploadedFiles', $response); 133 | $this->assertTrue(in_array('file', $response['uploadedFiles'])); 134 | $this->checkFilesExist($types); 135 | 136 | foreach ($types as $type) { 137 | $fileType = empty($type) ? $type : ".".$type; 138 | /** @var Response $response */ 139 | $response = Yii::$app->runAction('attachments/file/download-temp', ['filename' => "file$fileType"]); 140 | ob_start(); 141 | $response->send(); 142 | $actual = ob_get_clean(); 143 | $response->clear(); 144 | $expected = file_get_contents(Yii::getAlias("@tests/files/file$fileType")); 145 | $this->assertEquals($expected, $actual); 146 | 147 | $response = Yii::$app->runAction('attachments/file/delete-temp', ['filename' => "file$fileType"]); 148 | $this->assertEquals($response, []); 149 | } 150 | $this->checkFilesNotExist($types); 151 | $this->assertFileNotExists($this->getTempDirPath()); 152 | 153 | $comment = new Comment(); 154 | $comment->text = 'test'; 155 | $this->assertTrue($comment->save()); 156 | 157 | foreach ($comment->files as $file) { 158 | $fileType = empty($file->type) ? $file->type : ".".$file->type; 159 | /** @var Response $response */ 160 | $response = Yii::$app->runAction('attachments/file/download', ['id' => $file->id]); 161 | ob_start(); 162 | $response->send(); 163 | $actual = ob_get_clean(); 164 | $response->clear(); 165 | $expected = file_get_contents(Yii::getAlias("@tests/files/file{$fileType}")); 166 | $this->assertEquals($expected, $actual); 167 | $this->assertFileExists($file->path); 168 | $response = Yii::$app->runAction('attachments/file/delete', ['id' => -1]); 169 | $this->assertEquals($response, false); 170 | $response = Yii::$app->runAction('attachments/file/delete', ['id' => $file->id]); 171 | $this->assertEquals($response, true); 172 | $this->assertFileNotExists($file->path); 173 | } 174 | 175 | $this->assertNotSame(false, $comment->delete()); 176 | 177 | } 178 | 179 | } 180 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Yii2 attachments 2 | ================ 3 | [![Latest Stable Version](https://poser.pugx.org/nemmo/yii2-attachments/v/stable)](https://packagist.org/packages/nemmo/yii2-attachments) 4 | [![License](https://poser.pugx.org/nemmo/yii2-attachments/license)](https://packagist.org/packages/nemmo/yii2-attachments) 5 | [![Build Status](https://scrutinizer-ci.com/g/Nemmo/yii2-attachments/badges/build.png?b=tests)](https://scrutinizer-ci.com/g/Nemmo/yii2-attachments/build-status/tests) 6 | [![Code Coverage](https://scrutinizer-ci.com/g/Nemmo/yii2-attachments/badges/coverage.png?b=tests)](https://scrutinizer-ci.com/g/Nemmo/yii2-attachments/?branch=tests) 7 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/Nemmo/yii2-attachments/badges/quality-score.png?b=tests)](https://scrutinizer-ci.com/g/Nemmo/yii2-attachments/?branch=tests) 8 | [![Total Downloads](https://poser.pugx.org/nemmo/yii2-attachments/downloads)](https://packagist.org/packages/nemmo/yii2-attachments) 9 | 10 | Extension for file uploading and attaching to the models 11 | 12 | Demo 13 | ---- 14 | You can see the demo on the [krajee](http://plugins.krajee.com/file-input/demo) website 15 | 16 | Installation 17 | ------------ 18 | 19 | 1. The preferred way to install this extension is through [composer](http://getcomposer.org/download/). 20 | 21 | Either run 22 | 23 | ``` 24 | php composer.phar require nemmo/yii2-attachments "~1.0.1" 25 | ``` 26 | 27 | or add 28 | 29 | ``` 30 | "nemmo/yii2-attachments": "~1.0.1" 31 | ``` 32 | 33 | to the require section of your `composer.json` file. 34 | 35 | 2. Add module to `common/config/main.php` 36 | 37 | ```php 38 | 'modules' => [ 39 | ... 40 | 'attachments' => [ 41 | 'class' => nemmo\attachments\Module::className(), 42 | 'tempPath' => '@app/uploads/temp', 43 | 'storePath' => '@app/uploads/store', 44 | 'rules' => [ // Rules according to the FileValidator 45 | 'maxFiles' => 10, // Allow to upload maximum 3 files, default to 3 46 | 'mimeTypes' => 'image/png', // Only png images 47 | 'maxSize' => 1024 * 1024 // 1 MB 48 | ], 49 | 'tableName' => '{{%attachments}}' // Optional, default to 'attach_file' 50 | ] 51 | ... 52 | ] 53 | ``` 54 | 55 | 3. Apply migrations 56 | 57 | 58 | ```php 59 | 'controllerMap' => [ 60 | ... 61 | 'migrate' => [ 62 | 'class' => 'yii\console\controllers\MigrateController', 63 | 'migrationNamespaces' => [ 64 | 'nemmo\attachments\migrations', 65 | ], 66 | ], 67 | ... 68 | ], 69 | ``` 70 | 71 | ``` 72 | php yii migrate/up 73 | ``` 74 | 75 | 4. Attach behavior to your model (be sure that your model has "id" property) 76 | 77 | ```php 78 | public function behaviors() 79 | { 80 | return [ 81 | ... 82 | 'fileBehavior' => [ 83 | 'class' => \nemmo\attachments\behaviors\FileBehavior::className() 84 | ] 85 | ... 86 | ]; 87 | } 88 | ``` 89 | 90 | 5. Make sure that you have added `'enctype' => 'multipart/form-data'` to the ActiveForm options 91 | 92 | 6. Make sure that you specified `maxFiles` in module rules and `maxFileCount` on `AttachmentsInput` to the number that you want 93 | 94 | Usage 95 | ----- 96 | 97 | 1. In the `form.php` of your model add file input 98 | 99 | ```php 100 | 'file-input', // Optional 102 | 'model' => $model, 103 | 'options' => [ // Options of the Kartik's FileInput widget 104 | 'multiple' => true, // If you want to allow multiple upload, default to false 105 | ], 106 | 'pluginOptions' => [ // Plugin options of the Kartik's FileInput widget 107 | 'maxFileCount' => 10 // Client max files 108 | ] 109 | ]) ?> 110 | ``` 111 | 112 | 2. Use widget to show all attachments of the model in the `view.php` 113 | 114 | ```php 115 | $model, 117 | 'showDeleteButton' => false, // Optional. Default value is true 118 | ])?> 119 | ``` 120 | 121 | 3. (Deprecated) Add onclick action to your submit button that uploads all files before submitting form 122 | 123 | ```php 124 | isNewRecord ? 'Create' : 'Update', [ 125 | 'class' => $model->isNewRecord ? 'btn btn-success' : 'btn btn-primary', 126 | 'onclick' => "$('#file-input').fileinput('upload');" 127 | ]) ?> 128 | ``` 129 | 130 | 4. You can get all attached files by calling ```$model->files```, for example: 131 | 132 | ```php 133 | foreach ($model->files as $file) { 134 | echo $file->path; 135 | } 136 | ``` 137 | 138 | Using Events 139 | ------------ 140 | You may add the following function to your model 141 | 142 | ```php 143 | public function init(){ 144 | $this->on(\nemmo\attachments\behaviors\FileBehavior::EVENT_AFTER_ATTACH_FILES, function ($event) { 145 | /** @var $files \nemmo\attachments\models\File[] */ 146 | $files = $event->files; 147 | //your custom code 148 | }); 149 | parent::init(); 150 | } 151 | ``` 152 | 153 | FileController Route - No Download option. 154 | ---------- 155 | Its now possible to add "inline" to the filecontroller request. 156 | This is done using the $file->getUrl(true) function option. 157 | Use this in column actions if you want to directly display the file in the browser instead of download it. 158 | This also helps with preview configs as one no longer has to wrap the getUrl() in an image tag to stop the download of other filetypes. 159 | ``` 160 | ex: 161 | foreach ($this->getFiles() as $file) { 162 | $initialPreview[] =$file->getUrl(true); 163 | } 164 | ``` 165 | 166 | Change log 167 | ---------- 168 | - **Sept 13, 2023** - Added ability to open files inline. Upated initialPreviewConfig and AttachementsInput to use built in previews from Kartik. 169 | - **Dec 7, 2016** - Migration namespace coming with Yii 2.0.10. Release 1.0.0-beta.3. 170 | - **Apr 19, 2016** - Refactoring and testing. Ajax removing. Release 1.0.0-beta.2. 171 | - **Aug 17, 2015** - Support for prefix on table - you can specify the table name before migrating 172 | - **Jul 9, 2015** - Fixed automatic submitting form 173 | - **Jun 19, 2015** - Fixed uploading only files without submitting whole form and submitting form with ignoring upload errors 174 | - **May 1, 2015** - Fixed uploading when connection is slow or uploading time is long. Now ```onclick``` event on submit button is deprecated 175 | - **Apr 16, 2015** - Allow users to have a custom behavior class inheriting from FileBehavior. 176 | - **Apr 4, 2015** - Now all temp uploaded files will be deleted on every new form opened. 177 | - **Mar 16, 2015** - Fix: error in generating initial preview. Add: Getting path of the attached file by calling ```$file->path```. 178 | - **Mar 5, 2015** - Fix: restrictions for the number of maximum uploaded files. 179 | - **Mar 4, 2015** - Added restrictions for number of maximum uploaded files. 180 | - **Mar 3, 2015** - Fix of the file-input widget id. 181 | - **Feb 13, 2015** - Added restrictions to files (see point 1 in the Usage section), now use ```AttachmentsInput``` widget on the form view instead of ```FileInput``` 182 | - **Feb 11, 2015** - Added preview of uploaded but not saved files and ```tableOptions``` property for widget 183 | - **Feb 2, 2015** - Fix: all attached files will be deleted with the model. 184 | - **Feb 1, 2015** - AJAX or basic upload. 185 | - **Jan 30, 2015** - Several previews of images and other files, fix of required packages. 186 | - **Jan 29, 2015** - First version with basic uploading and previews. 187 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", 5 | "This file is @generated automatically" 6 | ], 7 | "hash": "f583c564952b02b22061619aa4b79667", 8 | "content-hash": "c02d2a7a2fcc84581354af9d87ee969b", 9 | "packages": [ 10 | { 11 | "name": "bower-asset/bootstrap", 12 | "version": "v3.3.5", 13 | "source": { 14 | "type": "git", 15 | "url": "https://github.com/twbs/bootstrap.git", 16 | "reference": "16b48259a62f576e52c903c476bd42b90ab22482" 17 | }, 18 | "dist": { 19 | "type": "zip", 20 | "url": "https://api.github.com/repos/twbs/bootstrap/zipball/16b48259a62f576e52c903c476bd42b90ab22482", 21 | "reference": "16b48259a62f576e52c903c476bd42b90ab22482", 22 | "shasum": "" 23 | }, 24 | "require": { 25 | "bower-asset/jquery": ">=1.9.1" 26 | }, 27 | "type": "bower-asset-library", 28 | "extra": { 29 | "bower-asset-main": [ 30 | "less/bootstrap.less", 31 | "dist/js/bootstrap.js" 32 | ], 33 | "bower-asset-ignore": [ 34 | "/.*", 35 | "_config.yml", 36 | "CNAME", 37 | "composer.json", 38 | "CONTRIBUTING.md", 39 | "docs", 40 | "js/tests", 41 | "test-infra" 42 | ] 43 | }, 44 | "license": [ 45 | "MIT" 46 | ], 47 | "description": "The most popular front-end framework for developing responsive, mobile first projects on the web.", 48 | "keywords": [ 49 | "css", 50 | "framework", 51 | "front-end", 52 | "js", 53 | "less", 54 | "mobile-first", 55 | "responsive", 56 | "web" 57 | ] 58 | }, 59 | { 60 | "name": "bower-asset/jquery", 61 | "version": "2.2.3", 62 | "source": { 63 | "type": "git", 64 | "url": "https://github.com/jquery/jquery-dist.git", 65 | "reference": "af22a351b2ea5801ffb1695abb3bb34d5bed9198" 66 | }, 67 | "dist": { 68 | "type": "zip", 69 | "url": "https://api.github.com/repos/jquery/jquery-dist/zipball/af22a351b2ea5801ffb1695abb3bb34d5bed9198", 70 | "reference": "af22a351b2ea5801ffb1695abb3bb34d5bed9198", 71 | "shasum": "" 72 | }, 73 | "type": "bower-asset-library", 74 | "extra": { 75 | "bower-asset-main": "dist/jquery.js", 76 | "bower-asset-ignore": [ 77 | "package.json" 78 | ] 79 | }, 80 | "license": [ 81 | "MIT" 82 | ], 83 | "keywords": [ 84 | "browser", 85 | "javascript", 86 | "jquery", 87 | "library" 88 | ] 89 | }, 90 | { 91 | "name": "bower-asset/jquery-colorbox", 92 | "version": "1.6.3", 93 | "source": { 94 | "type": "git", 95 | "url": "https://github.com/jackmoore/colorbox.git", 96 | "reference": "d9b74f2289b315c36b2e4b2bce01a799bf851aed" 97 | }, 98 | "dist": { 99 | "type": "zip", 100 | "url": "https://api.github.com/repos/jackmoore/colorbox/zipball/d9b74f2289b315c36b2e4b2bce01a799bf851aed", 101 | "reference": "d9b74f2289b315c36b2e4b2bce01a799bf851aed", 102 | "shasum": "" 103 | }, 104 | "require": { 105 | "bower-asset/jquery": ">=1.3.2" 106 | }, 107 | "type": "bower-asset-library", 108 | "extra": { 109 | "bower-asset-main": "jquery.colorbox.js", 110 | "bower-asset-ignore": [ 111 | "colorbox.jquery.json", 112 | "colorbox.ai", 113 | "content", 114 | "example1/index.html", 115 | "example2/index.html", 116 | "example3/index.html", 117 | "example4/index.html", 118 | "example5/index.html" 119 | ] 120 | }, 121 | "description": "jQuery lightbox and modal window plugin", 122 | "keywords": [ 123 | "gallery", 124 | "jquery-plugin", 125 | "lightbox", 126 | "modal", 127 | "popup", 128 | "ui" 129 | ] 130 | }, 131 | { 132 | "name": "bower-asset/jquery.inputmask", 133 | "version": "3.2.7", 134 | "source": { 135 | "type": "git", 136 | "url": "https://github.com/RobinHerbots/jquery.inputmask.git", 137 | "reference": "5a72c563b502b8e05958a524cdfffafe9987be38" 138 | }, 139 | "dist": { 140 | "type": "zip", 141 | "url": "https://api.github.com/repos/RobinHerbots/jquery.inputmask/zipball/5a72c563b502b8e05958a524cdfffafe9987be38", 142 | "reference": "5a72c563b502b8e05958a524cdfffafe9987be38", 143 | "shasum": "" 144 | }, 145 | "require": { 146 | "bower-asset/jquery": ">=1.7" 147 | }, 148 | "type": "bower-asset-library", 149 | "extra": { 150 | "bower-asset-main": [ 151 | "./dist/inputmask/inputmask.js" 152 | ], 153 | "bower-asset-ignore": [ 154 | "**/*", 155 | "!dist/*", 156 | "!dist/inputmask/*", 157 | "!dist/min/*", 158 | "!dist/min/inputmask/*", 159 | "!extra/bindings/*", 160 | "!extra/dependencyLibs/*", 161 | "!extra/phone-codes/*" 162 | ] 163 | }, 164 | "license": [ 165 | "http://opensource.org/licenses/mit-license.php" 166 | ], 167 | "description": "jquery.inputmask is a jquery plugin which create an input mask.", 168 | "keywords": [ 169 | "form", 170 | "input", 171 | "inputmask", 172 | "jquery", 173 | "mask", 174 | "plugins" 175 | ] 176 | }, 177 | { 178 | "name": "bower-asset/punycode", 179 | "version": "v1.3.2", 180 | "source": { 181 | "type": "git", 182 | "url": "https://github.com/bestiejs/punycode.js.git", 183 | "reference": "38c8d3131a82567bfef18da09f7f4db68c84f8a3" 184 | }, 185 | "dist": { 186 | "type": "zip", 187 | "url": "https://api.github.com/repos/bestiejs/punycode.js/zipball/38c8d3131a82567bfef18da09f7f4db68c84f8a3", 188 | "reference": "38c8d3131a82567bfef18da09f7f4db68c84f8a3", 189 | "shasum": "" 190 | }, 191 | "type": "bower-asset-library", 192 | "extra": { 193 | "bower-asset-main": "punycode.js", 194 | "bower-asset-ignore": [ 195 | "coverage", 196 | "tests", 197 | ".*", 198 | "component.json", 199 | "Gruntfile.js", 200 | "node_modules", 201 | "package.json" 202 | ] 203 | } 204 | }, 205 | { 206 | "name": "bower-asset/yii2-pjax", 207 | "version": "v2.0.6", 208 | "source": { 209 | "type": "git", 210 | "url": "https://github.com/yiisoft/jquery-pjax.git", 211 | "reference": "60728da6ade5879e807a49ce59ef9a72039b8978" 212 | }, 213 | "dist": { 214 | "type": "zip", 215 | "url": "https://api.github.com/repos/yiisoft/jquery-pjax/zipball/60728da6ade5879e807a49ce59ef9a72039b8978", 216 | "reference": "60728da6ade5879e807a49ce59ef9a72039b8978", 217 | "shasum": "" 218 | }, 219 | "require": { 220 | "bower-asset/jquery": ">=1.8" 221 | }, 222 | "type": "bower-asset-library", 223 | "extra": { 224 | "bower-asset-main": "./jquery.pjax.js", 225 | "bower-asset-ignore": [ 226 | ".travis.yml", 227 | "Gemfile", 228 | "Gemfile.lock", 229 | "CONTRIBUTING.md", 230 | "vendor/", 231 | "script/", 232 | "test/" 233 | ] 234 | }, 235 | "license": [ 236 | "MIT" 237 | ] 238 | }, 239 | { 240 | "name": "cebe/markdown", 241 | "version": "1.1.0", 242 | "source": { 243 | "type": "git", 244 | "url": "https://github.com/cebe/markdown.git", 245 | "reference": "54a2c49de31cc44e864ebf0500a35ef21d0010b2" 246 | }, 247 | "dist": { 248 | "type": "zip", 249 | "url": "https://api.github.com/repos/cebe/markdown/zipball/54a2c49de31cc44e864ebf0500a35ef21d0010b2", 250 | "reference": "54a2c49de31cc44e864ebf0500a35ef21d0010b2", 251 | "shasum": "" 252 | }, 253 | "require": { 254 | "lib-pcre": "*", 255 | "php": ">=5.4.0" 256 | }, 257 | "require-dev": { 258 | "cebe/indent": "*", 259 | "facebook/xhprof": "*@dev", 260 | "phpunit/phpunit": "4.1.*" 261 | }, 262 | "bin": [ 263 | "bin/markdown" 264 | ], 265 | "type": "library", 266 | "extra": { 267 | "branch-alias": { 268 | "dev-master": "1.1.x-dev" 269 | } 270 | }, 271 | "autoload": { 272 | "psr-4": { 273 | "cebe\\markdown\\": "" 274 | } 275 | }, 276 | "notification-url": "https://packagist.org/downloads/", 277 | "license": [ 278 | "MIT" 279 | ], 280 | "authors": [ 281 | { 282 | "name": "Carsten Brandt", 283 | "email": "mail@cebe.cc", 284 | "homepage": "http://cebe.cc/", 285 | "role": "Creator" 286 | } 287 | ], 288 | "description": "A super fast, highly extensible markdown parser for PHP", 289 | "homepage": "https://github.com/cebe/markdown#readme", 290 | "keywords": [ 291 | "extensible", 292 | "fast", 293 | "gfm", 294 | "markdown", 295 | "markdown-extra" 296 | ], 297 | "time": "2015-03-06 05:28:07" 298 | }, 299 | { 300 | "name": "ezyang/htmlpurifier", 301 | "version": "v4.6.0", 302 | "source": { 303 | "type": "git", 304 | "url": "https://github.com/ezyang/htmlpurifier.git", 305 | "reference": "6f389f0f25b90d0b495308efcfa073981177f0fd" 306 | }, 307 | "dist": { 308 | "type": "zip", 309 | "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/6f389f0f25b90d0b495308efcfa073981177f0fd", 310 | "reference": "6f389f0f25b90d0b495308efcfa073981177f0fd", 311 | "shasum": "" 312 | }, 313 | "require": { 314 | "php": ">=5.2" 315 | }, 316 | "type": "library", 317 | "autoload": { 318 | "psr-0": { 319 | "HTMLPurifier": "library/" 320 | }, 321 | "files": [ 322 | "library/HTMLPurifier.composer.php" 323 | ] 324 | }, 325 | "notification-url": "https://packagist.org/downloads/", 326 | "license": [ 327 | "LGPL" 328 | ], 329 | "authors": [ 330 | { 331 | "name": "Edward Z. Yang", 332 | "email": "admin@htmlpurifier.org", 333 | "homepage": "http://ezyang.com" 334 | } 335 | ], 336 | "description": "Standards compliant HTML filter written in PHP", 337 | "homepage": "http://htmlpurifier.org/", 338 | "keywords": [ 339 | "html" 340 | ], 341 | "time": "2013-11-30 08:25:19" 342 | }, 343 | { 344 | "name": "himiklab/yii2-colorbox-widget", 345 | "version": "1.0.3", 346 | "source": { 347 | "type": "git", 348 | "url": "https://github.com/himiklab/yii2-colorbox-widget.git", 349 | "reference": "c4aee292cdeabe03e996eb4396719376c2252602" 350 | }, 351 | "dist": { 352 | "type": "zip", 353 | "url": "https://api.github.com/repos/himiklab/yii2-colorbox-widget/zipball/c4aee292cdeabe03e996eb4396719376c2252602", 354 | "reference": "c4aee292cdeabe03e996eb4396719376c2252602", 355 | "shasum": "" 356 | }, 357 | "require": { 358 | "bower-asset/jquery-colorbox": "*", 359 | "yiisoft/yii2": "*" 360 | }, 361 | "type": "yii2-extension", 362 | "autoload": { 363 | "psr-4": { 364 | "himiklab\\colorbox\\": "" 365 | } 366 | }, 367 | "notification-url": "https://packagist.org/downloads/", 368 | "license": [ 369 | "MIT" 370 | ], 371 | "authors": [ 372 | { 373 | "name": "HimikLab", 374 | "homepage": "https://github.com/himiklab/" 375 | } 376 | ], 377 | "description": "Customizable lightbox widget for Yii2", 378 | "keywords": [ 379 | "colorbox", 380 | "lightbox", 381 | "widget", 382 | "yii2" 383 | ], 384 | "time": "2016-02-02 10:23:58" 385 | }, 386 | { 387 | "name": "kartik-v/bootstrap-fileinput", 388 | "version": "v4.3.1", 389 | "source": { 390 | "type": "git", 391 | "url": "https://github.com/kartik-v/bootstrap-fileinput.git", 392 | "reference": "32b0ccb2562b9c83f12222ddd1406986a45450d1" 393 | }, 394 | "dist": { 395 | "type": "zip", 396 | "url": "https://api.github.com/repos/kartik-v/bootstrap-fileinput/zipball/32b0ccb2562b9c83f12222ddd1406986a45450d1", 397 | "reference": "32b0ccb2562b9c83f12222ddd1406986a45450d1", 398 | "shasum": "" 399 | }, 400 | "type": "library", 401 | "extra": { 402 | "branch-alias": { 403 | "dev-master": "4.3.x-dev" 404 | } 405 | }, 406 | "autoload": { 407 | "psr-4": { 408 | "kartik\\plugins\\fileinput\\": "" 409 | } 410 | }, 411 | "notification-url": "https://packagist.org/downloads/", 412 | "license": [ 413 | "BSD-3-Clause" 414 | ], 415 | "authors": [ 416 | { 417 | "name": "Kartik Visweswaran", 418 | "email": "kartikv2@gmail.com", 419 | "homepage": "http://www.krajee.com/" 420 | } 421 | ], 422 | "description": "An enhanced HTML 5 file input for Bootstrap 3.x with features for file preview for many file types, multiple selection, ajax uploads, and more.", 423 | "homepage": "https://github.com/kartik-v/bootstrap-fileinput", 424 | "keywords": [ 425 | "ajax", 426 | "bootstrap", 427 | "delete", 428 | "file", 429 | "image", 430 | "input", 431 | "jquery", 432 | "multiple", 433 | "preview", 434 | "progress", 435 | "upload" 436 | ], 437 | "time": "2016-02-28 02:23:49" 438 | }, 439 | { 440 | "name": "kartik-v/yii2-krajee-base", 441 | "version": "v1.8.4", 442 | "source": { 443 | "type": "git", 444 | "url": "https://github.com/kartik-v/yii2-krajee-base.git", 445 | "reference": "3115b09aeb15a5e06f38dc16860baf153d9bf70e" 446 | }, 447 | "dist": { 448 | "type": "zip", 449 | "url": "https://api.github.com/repos/kartik-v/yii2-krajee-base/zipball/3115b09aeb15a5e06f38dc16860baf153d9bf70e", 450 | "reference": "3115b09aeb15a5e06f38dc16860baf153d9bf70e", 451 | "shasum": "" 452 | }, 453 | "require": { 454 | "yiisoft/yii2-bootstrap": "@dev" 455 | }, 456 | "type": "yii2-extension", 457 | "extra": { 458 | "branch-alias": { 459 | "dev-master": "1.8.x-dev" 460 | } 461 | }, 462 | "autoload": { 463 | "psr-4": { 464 | "kartik\\base\\": "" 465 | } 466 | }, 467 | "notification-url": "https://packagist.org/downloads/", 468 | "license": [ 469 | "BSD-3-Clause" 470 | ], 471 | "authors": [ 472 | { 473 | "name": "Kartik Visweswaran", 474 | "email": "kartikv2@gmail.com", 475 | "homepage": "http://www.krajee.com/" 476 | } 477 | ], 478 | "description": "Base library and foundation components for all Yii2 Krajee extensions.", 479 | "homepage": "https://github.com/kartik-v/yii2-krajee-base", 480 | "keywords": [ 481 | "base", 482 | "extension", 483 | "foundation", 484 | "krajee", 485 | "widget", 486 | "yii2" 487 | ], 488 | "time": "2016-04-11 09:07:40" 489 | }, 490 | { 491 | "name": "kartik-v/yii2-widget-fileinput", 492 | "version": "v1.0.4", 493 | "source": { 494 | "type": "git", 495 | "url": "https://github.com/kartik-v/yii2-widget-fileinput.git", 496 | "reference": "36f9f493c2d814529f2a195422a8af2e020fc80c" 497 | }, 498 | "dist": { 499 | "type": "zip", 500 | "url": "https://api.github.com/repos/kartik-v/yii2-widget-fileinput/zipball/36f9f493c2d814529f2a195422a8af2e020fc80c", 501 | "reference": "36f9f493c2d814529f2a195422a8af2e020fc80c", 502 | "shasum": "" 503 | }, 504 | "require": { 505 | "kartik-v/bootstrap-fileinput": "~4.2", 506 | "kartik-v/yii2-krajee-base": "~1.7" 507 | }, 508 | "type": "yii2-extension", 509 | "autoload": { 510 | "psr-4": { 511 | "kartik\\file\\": "" 512 | } 513 | }, 514 | "notification-url": "https://packagist.org/downloads/", 515 | "license": [ 516 | "BSD-3-Clause" 517 | ], 518 | "authors": [ 519 | { 520 | "name": "Kartik Visweswaran", 521 | "email": "kartikv2@gmail.com", 522 | "homepage": "http://www.krajee.com/" 523 | } 524 | ], 525 | "description": "An enhanced FileInput widget for Bootstrap 3.x with file preview, multiple selection, and more features (sub repo split from yii2-widgets)", 526 | "homepage": "https://github.com/kartik-v/yii2-widget-fileinput", 527 | "keywords": [ 528 | "extension", 529 | "file", 530 | "form", 531 | "input", 532 | "jquery", 533 | "plugin", 534 | "upload", 535 | "widget", 536 | "yii2" 537 | ], 538 | "time": "2016-01-10 16:10:58" 539 | }, 540 | { 541 | "name": "yiisoft/yii2", 542 | "version": "2.0.7", 543 | "source": { 544 | "type": "git", 545 | "url": "https://github.com/yiisoft/yii2-framework.git", 546 | "reference": "f45651582cb853b4326730d9d187a0f7a44a45a3" 547 | }, 548 | "dist": { 549 | "type": "zip", 550 | "url": "https://api.github.com/repos/yiisoft/yii2-framework/zipball/f45651582cb853b4326730d9d187a0f7a44a45a3", 551 | "reference": "f45651582cb853b4326730d9d187a0f7a44a45a3", 552 | "shasum": "" 553 | }, 554 | "require": { 555 | "bower-asset/jquery": "2.2.*@stable | 2.1.*@stable | 1.11.*@stable", 556 | "bower-asset/jquery.inputmask": "~3.2.2", 557 | "bower-asset/punycode": "1.3.*", 558 | "bower-asset/yii2-pjax": "~2.0.1", 559 | "cebe/markdown": "~1.0.0 | ~1.1.0", 560 | "ext-ctype": "*", 561 | "ext-mbstring": "*", 562 | "ezyang/htmlpurifier": "4.6.*", 563 | "lib-pcre": "*", 564 | "php": ">=5.4.0", 565 | "yiisoft/yii2-composer": "~2.0.4" 566 | }, 567 | "bin": [ 568 | "yii" 569 | ], 570 | "type": "library", 571 | "extra": { 572 | "branch-alias": { 573 | "dev-master": "2.0.x-dev" 574 | } 575 | }, 576 | "autoload": { 577 | "psr-4": { 578 | "yii\\": "" 579 | } 580 | }, 581 | "notification-url": "https://packagist.org/downloads/", 582 | "license": [ 583 | "BSD-3-Clause" 584 | ], 585 | "authors": [ 586 | { 587 | "name": "Qiang Xue", 588 | "email": "qiang.xue@gmail.com", 589 | "homepage": "http://www.yiiframework.com/", 590 | "role": "Founder and project lead" 591 | }, 592 | { 593 | "name": "Alexander Makarov", 594 | "email": "sam@rmcreative.ru", 595 | "homepage": "http://rmcreative.ru/", 596 | "role": "Core framework development" 597 | }, 598 | { 599 | "name": "Maurizio Domba", 600 | "homepage": "http://mdomba.info/", 601 | "role": "Core framework development" 602 | }, 603 | { 604 | "name": "Carsten Brandt", 605 | "email": "mail@cebe.cc", 606 | "homepage": "http://cebe.cc/", 607 | "role": "Core framework development" 608 | }, 609 | { 610 | "name": "Timur Ruziev", 611 | "email": "resurtm@gmail.com", 612 | "homepage": "http://resurtm.com/", 613 | "role": "Core framework development" 614 | }, 615 | { 616 | "name": "Paul Klimov", 617 | "email": "klimov.paul@gmail.com", 618 | "role": "Core framework development" 619 | }, 620 | { 621 | "name": "Dmitry Naumenko", 622 | "email": "d.naumenko.a@gmail.com", 623 | "role": "Core framework development" 624 | } 625 | ], 626 | "description": "Yii PHP Framework Version 2", 627 | "homepage": "http://www.yiiframework.com/", 628 | "keywords": [ 629 | "framework", 630 | "yii2" 631 | ], 632 | "time": "2016-02-14 14:45:55" 633 | }, 634 | { 635 | "name": "yiisoft/yii2-bootstrap", 636 | "version": "2.0.6", 637 | "source": { 638 | "type": "git", 639 | "url": "https://github.com/yiisoft/yii2-bootstrap.git", 640 | "reference": "3fd2b8c950cce79d60e9702d6bcb24eb3c80f6c5" 641 | }, 642 | "dist": { 643 | "type": "zip", 644 | "url": "https://api.github.com/repos/yiisoft/yii2-bootstrap/zipball/3fd2b8c950cce79d60e9702d6bcb24eb3c80f6c5", 645 | "reference": "3fd2b8c950cce79d60e9702d6bcb24eb3c80f6c5", 646 | "shasum": "" 647 | }, 648 | "require": { 649 | "bower-asset/bootstrap": "3.3.* | 3.2.* | 3.1.*", 650 | "yiisoft/yii2": ">=2.0.6" 651 | }, 652 | "type": "yii2-extension", 653 | "extra": { 654 | "branch-alias": { 655 | "dev-master": "2.0.x-dev" 656 | }, 657 | "asset-installer-paths": { 658 | "npm-asset-library": "vendor/npm", 659 | "bower-asset-library": "vendor/bower" 660 | } 661 | }, 662 | "autoload": { 663 | "psr-4": { 664 | "yii\\bootstrap\\": "" 665 | } 666 | }, 667 | "notification-url": "https://packagist.org/downloads/", 668 | "license": [ 669 | "BSD-3-Clause" 670 | ], 671 | "authors": [ 672 | { 673 | "name": "Qiang Xue", 674 | "email": "qiang.xue@gmail.com" 675 | } 676 | ], 677 | "description": "The Twitter Bootstrap extension for the Yii framework", 678 | "keywords": [ 679 | "bootstrap", 680 | "yii2" 681 | ], 682 | "time": "2016-03-17 03:29:28" 683 | }, 684 | { 685 | "name": "yiisoft/yii2-composer", 686 | "version": "2.0.4", 687 | "source": { 688 | "type": "git", 689 | "url": "https://github.com/yiisoft/yii2-composer.git", 690 | "reference": "7452fd908a5023b8bb5ea1b123a174ca080de464" 691 | }, 692 | "dist": { 693 | "type": "zip", 694 | "url": "https://api.github.com/repos/yiisoft/yii2-composer/zipball/7452fd908a5023b8bb5ea1b123a174ca080de464", 695 | "reference": "7452fd908a5023b8bb5ea1b123a174ca080de464", 696 | "shasum": "" 697 | }, 698 | "require": { 699 | "composer-plugin-api": "^1.0" 700 | }, 701 | "type": "composer-plugin", 702 | "extra": { 703 | "class": "yii\\composer\\Plugin", 704 | "branch-alias": { 705 | "dev-master": "2.0.x-dev" 706 | } 707 | }, 708 | "autoload": { 709 | "psr-4": { 710 | "yii\\composer\\": "" 711 | } 712 | }, 713 | "notification-url": "https://packagist.org/downloads/", 714 | "license": [ 715 | "BSD-3-Clause" 716 | ], 717 | "authors": [ 718 | { 719 | "name": "Qiang Xue", 720 | "email": "qiang.xue@gmail.com" 721 | } 722 | ], 723 | "description": "The composer plugin for Yii extension installer", 724 | "keywords": [ 725 | "composer", 726 | "extension installer", 727 | "yii2" 728 | ], 729 | "time": "2016-02-06 00:49:24" 730 | } 731 | ], 732 | "packages-dev": [ 733 | { 734 | "name": "doctrine/instantiator", 735 | "version": "1.0.5", 736 | "source": { 737 | "type": "git", 738 | "url": "https://github.com/doctrine/instantiator.git", 739 | "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d" 740 | }, 741 | "dist": { 742 | "type": "zip", 743 | "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d", 744 | "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d", 745 | "shasum": "" 746 | }, 747 | "require": { 748 | "php": ">=5.3,<8.0-DEV" 749 | }, 750 | "require-dev": { 751 | "athletic/athletic": "~0.1.8", 752 | "ext-pdo": "*", 753 | "ext-phar": "*", 754 | "phpunit/phpunit": "~4.0", 755 | "squizlabs/php_codesniffer": "~2.0" 756 | }, 757 | "type": "library", 758 | "extra": { 759 | "branch-alias": { 760 | "dev-master": "1.0.x-dev" 761 | } 762 | }, 763 | "autoload": { 764 | "psr-4": { 765 | "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" 766 | } 767 | }, 768 | "notification-url": "https://packagist.org/downloads/", 769 | "license": [ 770 | "MIT" 771 | ], 772 | "authors": [ 773 | { 774 | "name": "Marco Pivetta", 775 | "email": "ocramius@gmail.com", 776 | "homepage": "http://ocramius.github.com/" 777 | } 778 | ], 779 | "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", 780 | "homepage": "https://github.com/doctrine/instantiator", 781 | "keywords": [ 782 | "constructor", 783 | "instantiate" 784 | ], 785 | "time": "2015-06-14 21:17:01" 786 | }, 787 | { 788 | "name": "phpdocumentor/reflection-docblock", 789 | "version": "2.0.4", 790 | "source": { 791 | "type": "git", 792 | "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", 793 | "reference": "d68dbdc53dc358a816f00b300704702b2eaff7b8" 794 | }, 795 | "dist": { 796 | "type": "zip", 797 | "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/d68dbdc53dc358a816f00b300704702b2eaff7b8", 798 | "reference": "d68dbdc53dc358a816f00b300704702b2eaff7b8", 799 | "shasum": "" 800 | }, 801 | "require": { 802 | "php": ">=5.3.3" 803 | }, 804 | "require-dev": { 805 | "phpunit/phpunit": "~4.0" 806 | }, 807 | "suggest": { 808 | "dflydev/markdown": "~1.0", 809 | "erusev/parsedown": "~1.0" 810 | }, 811 | "type": "library", 812 | "extra": { 813 | "branch-alias": { 814 | "dev-master": "2.0.x-dev" 815 | } 816 | }, 817 | "autoload": { 818 | "psr-0": { 819 | "phpDocumentor": [ 820 | "src/" 821 | ] 822 | } 823 | }, 824 | "notification-url": "https://packagist.org/downloads/", 825 | "license": [ 826 | "MIT" 827 | ], 828 | "authors": [ 829 | { 830 | "name": "Mike van Riel", 831 | "email": "mike.vanriel@naenius.com" 832 | } 833 | ], 834 | "time": "2015-02-03 12:10:50" 835 | }, 836 | { 837 | "name": "phpspec/prophecy", 838 | "version": "v1.6.0", 839 | "source": { 840 | "type": "git", 841 | "url": "https://github.com/phpspec/prophecy.git", 842 | "reference": "3c91bdf81797d725b14cb62906f9a4ce44235972" 843 | }, 844 | "dist": { 845 | "type": "zip", 846 | "url": "https://api.github.com/repos/phpspec/prophecy/zipball/3c91bdf81797d725b14cb62906f9a4ce44235972", 847 | "reference": "3c91bdf81797d725b14cb62906f9a4ce44235972", 848 | "shasum": "" 849 | }, 850 | "require": { 851 | "doctrine/instantiator": "^1.0.2", 852 | "php": "^5.3|^7.0", 853 | "phpdocumentor/reflection-docblock": "~2.0", 854 | "sebastian/comparator": "~1.1", 855 | "sebastian/recursion-context": "~1.0" 856 | }, 857 | "require-dev": { 858 | "phpspec/phpspec": "~2.0" 859 | }, 860 | "type": "library", 861 | "extra": { 862 | "branch-alias": { 863 | "dev-master": "1.5.x-dev" 864 | } 865 | }, 866 | "autoload": { 867 | "psr-0": { 868 | "Prophecy\\": "src/" 869 | } 870 | }, 871 | "notification-url": "https://packagist.org/downloads/", 872 | "license": [ 873 | "MIT" 874 | ], 875 | "authors": [ 876 | { 877 | "name": "Konstantin Kudryashov", 878 | "email": "ever.zet@gmail.com", 879 | "homepage": "http://everzet.com" 880 | }, 881 | { 882 | "name": "Marcello Duarte", 883 | "email": "marcello.duarte@gmail.com" 884 | } 885 | ], 886 | "description": "Highly opinionated mocking framework for PHP 5.3+", 887 | "homepage": "https://github.com/phpspec/prophecy", 888 | "keywords": [ 889 | "Double", 890 | "Dummy", 891 | "fake", 892 | "mock", 893 | "spy", 894 | "stub" 895 | ], 896 | "time": "2016-02-15 07:46:21" 897 | }, 898 | { 899 | "name": "phpunit/dbunit", 900 | "version": "1.4.1", 901 | "source": { 902 | "type": "git", 903 | "url": "https://github.com/sebastianbergmann/dbunit.git", 904 | "reference": "9aaee6447663ff1b0cd50c23637e04af74c5e2ae" 905 | }, 906 | "dist": { 907 | "type": "zip", 908 | "url": "https://api.github.com/repos/sebastianbergmann/dbunit/zipball/9aaee6447663ff1b0cd50c23637e04af74c5e2ae", 909 | "reference": "9aaee6447663ff1b0cd50c23637e04af74c5e2ae", 910 | "shasum": "" 911 | }, 912 | "require": { 913 | "ext-pdo": "*", 914 | "ext-simplexml": "*", 915 | "php": ">=5.3.3", 916 | "phpunit/phpunit": "~4|~5", 917 | "symfony/yaml": "~2.1|~3.0" 918 | }, 919 | "bin": [ 920 | "composer/bin/dbunit" 921 | ], 922 | "type": "library", 923 | "extra": { 924 | "branch-alias": { 925 | "dev-master": "1.3.x-dev" 926 | } 927 | }, 928 | "autoload": { 929 | "classmap": [ 930 | "PHPUnit/" 931 | ] 932 | }, 933 | "notification-url": "https://packagist.org/downloads/", 934 | "include-path": [ 935 | "", 936 | "../../symfony/yaml/" 937 | ], 938 | "license": [ 939 | "BSD-3-Clause" 940 | ], 941 | "authors": [ 942 | { 943 | "name": "Sebastian Bergmann", 944 | "email": "sb@sebastian-bergmann.de", 945 | "role": "lead" 946 | } 947 | ], 948 | "description": "DbUnit port for PHP/PHPUnit to support database interaction testing.", 949 | "homepage": "https://github.com/sebastianbergmann/dbunit/", 950 | "keywords": [ 951 | "database", 952 | "testing", 953 | "xunit" 954 | ], 955 | "time": "2015-08-07 04:57:38" 956 | }, 957 | { 958 | "name": "phpunit/php-code-coverage", 959 | "version": "2.2.4", 960 | "source": { 961 | "type": "git", 962 | "url": "https://github.com/sebastianbergmann/php-code-coverage.git", 963 | "reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979" 964 | }, 965 | "dist": { 966 | "type": "zip", 967 | "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/eabf68b476ac7d0f73793aada060f1c1a9bf8979", 968 | "reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979", 969 | "shasum": "" 970 | }, 971 | "require": { 972 | "php": ">=5.3.3", 973 | "phpunit/php-file-iterator": "~1.3", 974 | "phpunit/php-text-template": "~1.2", 975 | "phpunit/php-token-stream": "~1.3", 976 | "sebastian/environment": "^1.3.2", 977 | "sebastian/version": "~1.0" 978 | }, 979 | "require-dev": { 980 | "ext-xdebug": ">=2.1.4", 981 | "phpunit/phpunit": "~4" 982 | }, 983 | "suggest": { 984 | "ext-dom": "*", 985 | "ext-xdebug": ">=2.2.1", 986 | "ext-xmlwriter": "*" 987 | }, 988 | "type": "library", 989 | "extra": { 990 | "branch-alias": { 991 | "dev-master": "2.2.x-dev" 992 | } 993 | }, 994 | "autoload": { 995 | "classmap": [ 996 | "src/" 997 | ] 998 | }, 999 | "notification-url": "https://packagist.org/downloads/", 1000 | "license": [ 1001 | "BSD-3-Clause" 1002 | ], 1003 | "authors": [ 1004 | { 1005 | "name": "Sebastian Bergmann", 1006 | "email": "sb@sebastian-bergmann.de", 1007 | "role": "lead" 1008 | } 1009 | ], 1010 | "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", 1011 | "homepage": "https://github.com/sebastianbergmann/php-code-coverage", 1012 | "keywords": [ 1013 | "coverage", 1014 | "testing", 1015 | "xunit" 1016 | ], 1017 | "time": "2015-10-06 15:47:00" 1018 | }, 1019 | { 1020 | "name": "phpunit/php-file-iterator", 1021 | "version": "1.4.1", 1022 | "source": { 1023 | "type": "git", 1024 | "url": "https://github.com/sebastianbergmann/php-file-iterator.git", 1025 | "reference": "6150bf2c35d3fc379e50c7602b75caceaa39dbf0" 1026 | }, 1027 | "dist": { 1028 | "type": "zip", 1029 | "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/6150bf2c35d3fc379e50c7602b75caceaa39dbf0", 1030 | "reference": "6150bf2c35d3fc379e50c7602b75caceaa39dbf0", 1031 | "shasum": "" 1032 | }, 1033 | "require": { 1034 | "php": ">=5.3.3" 1035 | }, 1036 | "type": "library", 1037 | "extra": { 1038 | "branch-alias": { 1039 | "dev-master": "1.4.x-dev" 1040 | } 1041 | }, 1042 | "autoload": { 1043 | "classmap": [ 1044 | "src/" 1045 | ] 1046 | }, 1047 | "notification-url": "https://packagist.org/downloads/", 1048 | "license": [ 1049 | "BSD-3-Clause" 1050 | ], 1051 | "authors": [ 1052 | { 1053 | "name": "Sebastian Bergmann", 1054 | "email": "sb@sebastian-bergmann.de", 1055 | "role": "lead" 1056 | } 1057 | ], 1058 | "description": "FilterIterator implementation that filters files based on a list of suffixes.", 1059 | "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", 1060 | "keywords": [ 1061 | "filesystem", 1062 | "iterator" 1063 | ], 1064 | "time": "2015-06-21 13:08:43" 1065 | }, 1066 | { 1067 | "name": "phpunit/php-text-template", 1068 | "version": "1.2.1", 1069 | "source": { 1070 | "type": "git", 1071 | "url": "https://github.com/sebastianbergmann/php-text-template.git", 1072 | "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" 1073 | }, 1074 | "dist": { 1075 | "type": "zip", 1076 | "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", 1077 | "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", 1078 | "shasum": "" 1079 | }, 1080 | "require": { 1081 | "php": ">=5.3.3" 1082 | }, 1083 | "type": "library", 1084 | "autoload": { 1085 | "classmap": [ 1086 | "src/" 1087 | ] 1088 | }, 1089 | "notification-url": "https://packagist.org/downloads/", 1090 | "license": [ 1091 | "BSD-3-Clause" 1092 | ], 1093 | "authors": [ 1094 | { 1095 | "name": "Sebastian Bergmann", 1096 | "email": "sebastian@phpunit.de", 1097 | "role": "lead" 1098 | } 1099 | ], 1100 | "description": "Simple template engine.", 1101 | "homepage": "https://github.com/sebastianbergmann/php-text-template/", 1102 | "keywords": [ 1103 | "template" 1104 | ], 1105 | "time": "2015-06-21 13:50:34" 1106 | }, 1107 | { 1108 | "name": "phpunit/php-timer", 1109 | "version": "1.0.7", 1110 | "source": { 1111 | "type": "git", 1112 | "url": "https://github.com/sebastianbergmann/php-timer.git", 1113 | "reference": "3e82f4e9fc92665fafd9157568e4dcb01d014e5b" 1114 | }, 1115 | "dist": { 1116 | "type": "zip", 1117 | "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3e82f4e9fc92665fafd9157568e4dcb01d014e5b", 1118 | "reference": "3e82f4e9fc92665fafd9157568e4dcb01d014e5b", 1119 | "shasum": "" 1120 | }, 1121 | "require": { 1122 | "php": ">=5.3.3" 1123 | }, 1124 | "type": "library", 1125 | "autoload": { 1126 | "classmap": [ 1127 | "src/" 1128 | ] 1129 | }, 1130 | "notification-url": "https://packagist.org/downloads/", 1131 | "license": [ 1132 | "BSD-3-Clause" 1133 | ], 1134 | "authors": [ 1135 | { 1136 | "name": "Sebastian Bergmann", 1137 | "email": "sb@sebastian-bergmann.de", 1138 | "role": "lead" 1139 | } 1140 | ], 1141 | "description": "Utility class for timing", 1142 | "homepage": "https://github.com/sebastianbergmann/php-timer/", 1143 | "keywords": [ 1144 | "timer" 1145 | ], 1146 | "time": "2015-06-21 08:01:12" 1147 | }, 1148 | { 1149 | "name": "phpunit/php-token-stream", 1150 | "version": "1.4.8", 1151 | "source": { 1152 | "type": "git", 1153 | "url": "https://github.com/sebastianbergmann/php-token-stream.git", 1154 | "reference": "3144ae21711fb6cac0b1ab4cbe63b75ce3d4e8da" 1155 | }, 1156 | "dist": { 1157 | "type": "zip", 1158 | "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/3144ae21711fb6cac0b1ab4cbe63b75ce3d4e8da", 1159 | "reference": "3144ae21711fb6cac0b1ab4cbe63b75ce3d4e8da", 1160 | "shasum": "" 1161 | }, 1162 | "require": { 1163 | "ext-tokenizer": "*", 1164 | "php": ">=5.3.3" 1165 | }, 1166 | "require-dev": { 1167 | "phpunit/phpunit": "~4.2" 1168 | }, 1169 | "type": "library", 1170 | "extra": { 1171 | "branch-alias": { 1172 | "dev-master": "1.4-dev" 1173 | } 1174 | }, 1175 | "autoload": { 1176 | "classmap": [ 1177 | "src/" 1178 | ] 1179 | }, 1180 | "notification-url": "https://packagist.org/downloads/", 1181 | "license": [ 1182 | "BSD-3-Clause" 1183 | ], 1184 | "authors": [ 1185 | { 1186 | "name": "Sebastian Bergmann", 1187 | "email": "sebastian@phpunit.de" 1188 | } 1189 | ], 1190 | "description": "Wrapper around PHP's tokenizer extension.", 1191 | "homepage": "https://github.com/sebastianbergmann/php-token-stream/", 1192 | "keywords": [ 1193 | "tokenizer" 1194 | ], 1195 | "time": "2015-09-15 10:49:45" 1196 | }, 1197 | { 1198 | "name": "phpunit/phpunit", 1199 | "version": "4.8.24", 1200 | "source": { 1201 | "type": "git", 1202 | "url": "https://github.com/sebastianbergmann/phpunit.git", 1203 | "reference": "a1066c562c52900a142a0e2bbf0582994671385e" 1204 | }, 1205 | "dist": { 1206 | "type": "zip", 1207 | "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a1066c562c52900a142a0e2bbf0582994671385e", 1208 | "reference": "a1066c562c52900a142a0e2bbf0582994671385e", 1209 | "shasum": "" 1210 | }, 1211 | "require": { 1212 | "ext-dom": "*", 1213 | "ext-json": "*", 1214 | "ext-pcre": "*", 1215 | "ext-reflection": "*", 1216 | "ext-spl": "*", 1217 | "php": ">=5.3.3", 1218 | "phpspec/prophecy": "^1.3.1", 1219 | "phpunit/php-code-coverage": "~2.1", 1220 | "phpunit/php-file-iterator": "~1.4", 1221 | "phpunit/php-text-template": "~1.2", 1222 | "phpunit/php-timer": ">=1.0.6", 1223 | "phpunit/phpunit-mock-objects": "~2.3", 1224 | "sebastian/comparator": "~1.1", 1225 | "sebastian/diff": "~1.2", 1226 | "sebastian/environment": "~1.3", 1227 | "sebastian/exporter": "~1.2", 1228 | "sebastian/global-state": "~1.0", 1229 | "sebastian/version": "~1.0", 1230 | "symfony/yaml": "~2.1|~3.0" 1231 | }, 1232 | "suggest": { 1233 | "phpunit/php-invoker": "~1.1" 1234 | }, 1235 | "bin": [ 1236 | "phpunit" 1237 | ], 1238 | "type": "library", 1239 | "extra": { 1240 | "branch-alias": { 1241 | "dev-master": "4.8.x-dev" 1242 | } 1243 | }, 1244 | "autoload": { 1245 | "classmap": [ 1246 | "src/" 1247 | ] 1248 | }, 1249 | "notification-url": "https://packagist.org/downloads/", 1250 | "license": [ 1251 | "BSD-3-Clause" 1252 | ], 1253 | "authors": [ 1254 | { 1255 | "name": "Sebastian Bergmann", 1256 | "email": "sebastian@phpunit.de", 1257 | "role": "lead" 1258 | } 1259 | ], 1260 | "description": "The PHP Unit Testing framework.", 1261 | "homepage": "https://phpunit.de/", 1262 | "keywords": [ 1263 | "phpunit", 1264 | "testing", 1265 | "xunit" 1266 | ], 1267 | "time": "2016-03-14 06:16:08" 1268 | }, 1269 | { 1270 | "name": "phpunit/phpunit-mock-objects", 1271 | "version": "2.3.8", 1272 | "source": { 1273 | "type": "git", 1274 | "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", 1275 | "reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983" 1276 | }, 1277 | "dist": { 1278 | "type": "zip", 1279 | "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/ac8e7a3db35738d56ee9a76e78a4e03d97628983", 1280 | "reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983", 1281 | "shasum": "" 1282 | }, 1283 | "require": { 1284 | "doctrine/instantiator": "^1.0.2", 1285 | "php": ">=5.3.3", 1286 | "phpunit/php-text-template": "~1.2", 1287 | "sebastian/exporter": "~1.2" 1288 | }, 1289 | "require-dev": { 1290 | "phpunit/phpunit": "~4.4" 1291 | }, 1292 | "suggest": { 1293 | "ext-soap": "*" 1294 | }, 1295 | "type": "library", 1296 | "extra": { 1297 | "branch-alias": { 1298 | "dev-master": "2.3.x-dev" 1299 | } 1300 | }, 1301 | "autoload": { 1302 | "classmap": [ 1303 | "src/" 1304 | ] 1305 | }, 1306 | "notification-url": "https://packagist.org/downloads/", 1307 | "license": [ 1308 | "BSD-3-Clause" 1309 | ], 1310 | "authors": [ 1311 | { 1312 | "name": "Sebastian Bergmann", 1313 | "email": "sb@sebastian-bergmann.de", 1314 | "role": "lead" 1315 | } 1316 | ], 1317 | "description": "Mock Object library for PHPUnit", 1318 | "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", 1319 | "keywords": [ 1320 | "mock", 1321 | "xunit" 1322 | ], 1323 | "time": "2015-10-02 06:51:40" 1324 | }, 1325 | { 1326 | "name": "sebastian/comparator", 1327 | "version": "1.2.0", 1328 | "source": { 1329 | "type": "git", 1330 | "url": "https://github.com/sebastianbergmann/comparator.git", 1331 | "reference": "937efb279bd37a375bcadf584dec0726f84dbf22" 1332 | }, 1333 | "dist": { 1334 | "type": "zip", 1335 | "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/937efb279bd37a375bcadf584dec0726f84dbf22", 1336 | "reference": "937efb279bd37a375bcadf584dec0726f84dbf22", 1337 | "shasum": "" 1338 | }, 1339 | "require": { 1340 | "php": ">=5.3.3", 1341 | "sebastian/diff": "~1.2", 1342 | "sebastian/exporter": "~1.2" 1343 | }, 1344 | "require-dev": { 1345 | "phpunit/phpunit": "~4.4" 1346 | }, 1347 | "type": "library", 1348 | "extra": { 1349 | "branch-alias": { 1350 | "dev-master": "1.2.x-dev" 1351 | } 1352 | }, 1353 | "autoload": { 1354 | "classmap": [ 1355 | "src/" 1356 | ] 1357 | }, 1358 | "notification-url": "https://packagist.org/downloads/", 1359 | "license": [ 1360 | "BSD-3-Clause" 1361 | ], 1362 | "authors": [ 1363 | { 1364 | "name": "Jeff Welch", 1365 | "email": "whatthejeff@gmail.com" 1366 | }, 1367 | { 1368 | "name": "Volker Dusch", 1369 | "email": "github@wallbash.com" 1370 | }, 1371 | { 1372 | "name": "Bernhard Schussek", 1373 | "email": "bschussek@2bepublished.at" 1374 | }, 1375 | { 1376 | "name": "Sebastian Bergmann", 1377 | "email": "sebastian@phpunit.de" 1378 | } 1379 | ], 1380 | "description": "Provides the functionality to compare PHP values for equality", 1381 | "homepage": "http://www.github.com/sebastianbergmann/comparator", 1382 | "keywords": [ 1383 | "comparator", 1384 | "compare", 1385 | "equality" 1386 | ], 1387 | "time": "2015-07-26 15:48:44" 1388 | }, 1389 | { 1390 | "name": "sebastian/diff", 1391 | "version": "1.4.1", 1392 | "source": { 1393 | "type": "git", 1394 | "url": "https://github.com/sebastianbergmann/diff.git", 1395 | "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e" 1396 | }, 1397 | "dist": { 1398 | "type": "zip", 1399 | "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/13edfd8706462032c2f52b4b862974dd46b71c9e", 1400 | "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e", 1401 | "shasum": "" 1402 | }, 1403 | "require": { 1404 | "php": ">=5.3.3" 1405 | }, 1406 | "require-dev": { 1407 | "phpunit/phpunit": "~4.8" 1408 | }, 1409 | "type": "library", 1410 | "extra": { 1411 | "branch-alias": { 1412 | "dev-master": "1.4-dev" 1413 | } 1414 | }, 1415 | "autoload": { 1416 | "classmap": [ 1417 | "src/" 1418 | ] 1419 | }, 1420 | "notification-url": "https://packagist.org/downloads/", 1421 | "license": [ 1422 | "BSD-3-Clause" 1423 | ], 1424 | "authors": [ 1425 | { 1426 | "name": "Kore Nordmann", 1427 | "email": "mail@kore-nordmann.de" 1428 | }, 1429 | { 1430 | "name": "Sebastian Bergmann", 1431 | "email": "sebastian@phpunit.de" 1432 | } 1433 | ], 1434 | "description": "Diff implementation", 1435 | "homepage": "https://github.com/sebastianbergmann/diff", 1436 | "keywords": [ 1437 | "diff" 1438 | ], 1439 | "time": "2015-12-08 07:14:41" 1440 | }, 1441 | { 1442 | "name": "sebastian/environment", 1443 | "version": "1.3.5", 1444 | "source": { 1445 | "type": "git", 1446 | "url": "https://github.com/sebastianbergmann/environment.git", 1447 | "reference": "dc7a29032cf72b54f36dac15a1ca5b3a1b6029bf" 1448 | }, 1449 | "dist": { 1450 | "type": "zip", 1451 | "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/dc7a29032cf72b54f36dac15a1ca5b3a1b6029bf", 1452 | "reference": "dc7a29032cf72b54f36dac15a1ca5b3a1b6029bf", 1453 | "shasum": "" 1454 | }, 1455 | "require": { 1456 | "php": ">=5.3.3" 1457 | }, 1458 | "require-dev": { 1459 | "phpunit/phpunit": "~4.4" 1460 | }, 1461 | "type": "library", 1462 | "extra": { 1463 | "branch-alias": { 1464 | "dev-master": "1.3.x-dev" 1465 | } 1466 | }, 1467 | "autoload": { 1468 | "classmap": [ 1469 | "src/" 1470 | ] 1471 | }, 1472 | "notification-url": "https://packagist.org/downloads/", 1473 | "license": [ 1474 | "BSD-3-Clause" 1475 | ], 1476 | "authors": [ 1477 | { 1478 | "name": "Sebastian Bergmann", 1479 | "email": "sebastian@phpunit.de" 1480 | } 1481 | ], 1482 | "description": "Provides functionality to handle HHVM/PHP environments", 1483 | "homepage": "http://www.github.com/sebastianbergmann/environment", 1484 | "keywords": [ 1485 | "Xdebug", 1486 | "environment", 1487 | "hhvm" 1488 | ], 1489 | "time": "2016-02-26 18:40:46" 1490 | }, 1491 | { 1492 | "name": "sebastian/exporter", 1493 | "version": "1.2.1", 1494 | "source": { 1495 | "type": "git", 1496 | "url": "https://github.com/sebastianbergmann/exporter.git", 1497 | "reference": "7ae5513327cb536431847bcc0c10edba2701064e" 1498 | }, 1499 | "dist": { 1500 | "type": "zip", 1501 | "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/7ae5513327cb536431847bcc0c10edba2701064e", 1502 | "reference": "7ae5513327cb536431847bcc0c10edba2701064e", 1503 | "shasum": "" 1504 | }, 1505 | "require": { 1506 | "php": ">=5.3.3", 1507 | "sebastian/recursion-context": "~1.0" 1508 | }, 1509 | "require-dev": { 1510 | "phpunit/phpunit": "~4.4" 1511 | }, 1512 | "type": "library", 1513 | "extra": { 1514 | "branch-alias": { 1515 | "dev-master": "1.2.x-dev" 1516 | } 1517 | }, 1518 | "autoload": { 1519 | "classmap": [ 1520 | "src/" 1521 | ] 1522 | }, 1523 | "notification-url": "https://packagist.org/downloads/", 1524 | "license": [ 1525 | "BSD-3-Clause" 1526 | ], 1527 | "authors": [ 1528 | { 1529 | "name": "Jeff Welch", 1530 | "email": "whatthejeff@gmail.com" 1531 | }, 1532 | { 1533 | "name": "Volker Dusch", 1534 | "email": "github@wallbash.com" 1535 | }, 1536 | { 1537 | "name": "Bernhard Schussek", 1538 | "email": "bschussek@2bepublished.at" 1539 | }, 1540 | { 1541 | "name": "Sebastian Bergmann", 1542 | "email": "sebastian@phpunit.de" 1543 | }, 1544 | { 1545 | "name": "Adam Harvey", 1546 | "email": "aharvey@php.net" 1547 | } 1548 | ], 1549 | "description": "Provides the functionality to export PHP variables for visualization", 1550 | "homepage": "http://www.github.com/sebastianbergmann/exporter", 1551 | "keywords": [ 1552 | "export", 1553 | "exporter" 1554 | ], 1555 | "time": "2015-06-21 07:55:53" 1556 | }, 1557 | { 1558 | "name": "sebastian/global-state", 1559 | "version": "1.1.1", 1560 | "source": { 1561 | "type": "git", 1562 | "url": "https://github.com/sebastianbergmann/global-state.git", 1563 | "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4" 1564 | }, 1565 | "dist": { 1566 | "type": "zip", 1567 | "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bc37d50fea7d017d3d340f230811c9f1d7280af4", 1568 | "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4", 1569 | "shasum": "" 1570 | }, 1571 | "require": { 1572 | "php": ">=5.3.3" 1573 | }, 1574 | "require-dev": { 1575 | "phpunit/phpunit": "~4.2" 1576 | }, 1577 | "suggest": { 1578 | "ext-uopz": "*" 1579 | }, 1580 | "type": "library", 1581 | "extra": { 1582 | "branch-alias": { 1583 | "dev-master": "1.0-dev" 1584 | } 1585 | }, 1586 | "autoload": { 1587 | "classmap": [ 1588 | "src/" 1589 | ] 1590 | }, 1591 | "notification-url": "https://packagist.org/downloads/", 1592 | "license": [ 1593 | "BSD-3-Clause" 1594 | ], 1595 | "authors": [ 1596 | { 1597 | "name": "Sebastian Bergmann", 1598 | "email": "sebastian@phpunit.de" 1599 | } 1600 | ], 1601 | "description": "Snapshotting of global state", 1602 | "homepage": "http://www.github.com/sebastianbergmann/global-state", 1603 | "keywords": [ 1604 | "global state" 1605 | ], 1606 | "time": "2015-10-12 03:26:01" 1607 | }, 1608 | { 1609 | "name": "sebastian/recursion-context", 1610 | "version": "1.0.2", 1611 | "source": { 1612 | "type": "git", 1613 | "url": "https://github.com/sebastianbergmann/recursion-context.git", 1614 | "reference": "913401df809e99e4f47b27cdd781f4a258d58791" 1615 | }, 1616 | "dist": { 1617 | "type": "zip", 1618 | "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/913401df809e99e4f47b27cdd781f4a258d58791", 1619 | "reference": "913401df809e99e4f47b27cdd781f4a258d58791", 1620 | "shasum": "" 1621 | }, 1622 | "require": { 1623 | "php": ">=5.3.3" 1624 | }, 1625 | "require-dev": { 1626 | "phpunit/phpunit": "~4.4" 1627 | }, 1628 | "type": "library", 1629 | "extra": { 1630 | "branch-alias": { 1631 | "dev-master": "1.0.x-dev" 1632 | } 1633 | }, 1634 | "autoload": { 1635 | "classmap": [ 1636 | "src/" 1637 | ] 1638 | }, 1639 | "notification-url": "https://packagist.org/downloads/", 1640 | "license": [ 1641 | "BSD-3-Clause" 1642 | ], 1643 | "authors": [ 1644 | { 1645 | "name": "Jeff Welch", 1646 | "email": "whatthejeff@gmail.com" 1647 | }, 1648 | { 1649 | "name": "Sebastian Bergmann", 1650 | "email": "sebastian@phpunit.de" 1651 | }, 1652 | { 1653 | "name": "Adam Harvey", 1654 | "email": "aharvey@php.net" 1655 | } 1656 | ], 1657 | "description": "Provides functionality to recursively process PHP variables", 1658 | "homepage": "http://www.github.com/sebastianbergmann/recursion-context", 1659 | "time": "2015-11-11 19:50:13" 1660 | }, 1661 | { 1662 | "name": "sebastian/version", 1663 | "version": "1.0.6", 1664 | "source": { 1665 | "type": "git", 1666 | "url": "https://github.com/sebastianbergmann/version.git", 1667 | "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6" 1668 | }, 1669 | "dist": { 1670 | "type": "zip", 1671 | "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/58b3a85e7999757d6ad81c787a1fbf5ff6c628c6", 1672 | "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6", 1673 | "shasum": "" 1674 | }, 1675 | "type": "library", 1676 | "autoload": { 1677 | "classmap": [ 1678 | "src/" 1679 | ] 1680 | }, 1681 | "notification-url": "https://packagist.org/downloads/", 1682 | "license": [ 1683 | "BSD-3-Clause" 1684 | ], 1685 | "authors": [ 1686 | { 1687 | "name": "Sebastian Bergmann", 1688 | "email": "sebastian@phpunit.de", 1689 | "role": "lead" 1690 | } 1691 | ], 1692 | "description": "Library that helps with managing the version number of Git-hosted PHP projects", 1693 | "homepage": "https://github.com/sebastianbergmann/version", 1694 | "time": "2015-06-21 13:59:46" 1695 | }, 1696 | { 1697 | "name": "symfony/yaml", 1698 | "version": "v3.0.4", 1699 | "source": { 1700 | "type": "git", 1701 | "url": "https://github.com/symfony/yaml.git", 1702 | "reference": "0047c8366744a16de7516622c5b7355336afae96" 1703 | }, 1704 | "dist": { 1705 | "type": "zip", 1706 | "url": "https://api.github.com/repos/symfony/yaml/zipball/0047c8366744a16de7516622c5b7355336afae96", 1707 | "reference": "0047c8366744a16de7516622c5b7355336afae96", 1708 | "shasum": "" 1709 | }, 1710 | "require": { 1711 | "php": ">=5.5.9" 1712 | }, 1713 | "type": "library", 1714 | "extra": { 1715 | "branch-alias": { 1716 | "dev-master": "3.0-dev" 1717 | } 1718 | }, 1719 | "autoload": { 1720 | "psr-4": { 1721 | "Symfony\\Component\\Yaml\\": "" 1722 | }, 1723 | "exclude-from-classmap": [ 1724 | "/Tests/" 1725 | ] 1726 | }, 1727 | "notification-url": "https://packagist.org/downloads/", 1728 | "license": [ 1729 | "MIT" 1730 | ], 1731 | "authors": [ 1732 | { 1733 | "name": "Fabien Potencier", 1734 | "email": "fabien@symfony.com" 1735 | }, 1736 | { 1737 | "name": "Symfony Community", 1738 | "homepage": "https://symfony.com/contributors" 1739 | } 1740 | ], 1741 | "description": "Symfony Yaml Component", 1742 | "homepage": "https://symfony.com", 1743 | "time": "2016-03-04 07:55:57" 1744 | } 1745 | ], 1746 | "aliases": [], 1747 | "minimum-stability": "stable", 1748 | "stability-flags": [], 1749 | "prefer-stable": false, 1750 | "prefer-lowest": false, 1751 | "platform": { 1752 | "php": ">=5.4.0" 1753 | }, 1754 | "platform-dev": [] 1755 | } 1756 | --------------------------------------------------------------------------------