📜 ⬆️ ⬇️

Uploading files to Yii

Having written a number of projects on Yii, I thought about a convenient mechanism for working with downloaded files. Yii offers a set of tools for this purpose, but there is no single mechanism. In this article I want to offer the idea of ​​centralized processing of downloaded files in Yii.

The task boils down to the following. You need a convenient mechanism for uploading files and pictures to the server (including the necessary checks), creating thumbnails for pictures and automatically generating img tags and links to download the file. There is nothing suitable in the extensions . The closest in meaning is the upload extension, but it also does not provide a number of necessary functions. So I decided to write myself. Right before the publication of the article, I accidentally saw a recipe , in which a similar idea was considered.

The code examples are for reference only, I inserted them from a working project, but could confuse something. If you need a working code, at the end of the article there is a link to the project. So, go ahead!
')

The first iteration. We supplement the standard functionality.


What does Yii offer? First, the CUploadedFile class, which provides information about the uploaded file and allows you to save it to the server. Secondly, the CFileValidator validator, which checks the loaded file. Here is how the official documentation recommends downloading files:

 //  class MyModel extends CActiveRecord { public $image; public function rules(){ return array( array('image', 'file', 'types'=>'jpg, gif, png'), ); } } //  class MyModelController extends CController { public function actionCreate(){ $modMyModel=new MyModel; if(isset($_POST['MyModel'])){ $modMyModel->attributes=$_POST['MyModel']; $modMyModel->id_image=CUploadedFile::getInstance($modMyModel,'image'); if($modMyModel->save()){ $modMyModel->id_image->saveAs('path/to/localFile'); //   ,     //   } } $this->render('create', array('model'=>$modMyModel)); } } //  <?php echo CHtml::form('','post',array('enctype'=>'multipart/form-data')); ?> ... <?php echo CHtml::activeFileField($modMyModel, 'image'); ?> ... <?php echo CHtml::endForm(); ?> 


This approach has several disadvantages:

  1. In the framework there is no dedicated folder for uploading files.
  2. File downloads have to be described each time in the controller.
  3. This approach cannot be transferred to actionUpdate() , because it waits for the file to be loaded with each call. And it would be convenient to work with files as with ordinary properties - load when creating a model and, if necessary, reload when it changes.
  4. There are no recommendations regarding the subsequent access to the file. However, path/to/localFile can be stored in the model property.


Of course, I speak of these shortcomings only in the context of my own projects. And that's what I want to offer.

To begin with we will decide on the place for preservation of files. In my opinion, the directory .../protected/data/files best place to store .../protected/data/files . Generally speaking, for files created in the course of work, there is a folder .../protected/runtime , but according to the meaning of the data directory is more suitable for these purposes. The file name will be generated randomly ( uniqid() ) and stored in the model property $modMyModel->id_image , in the following paragraphs I will tell you how. True, this approach has one pitfall - the data directory is closed for accessing from the browser. How to deal with this - a little later. Looking ahead, I propose to upload files dynamically via readfile() , and to publish pictures (more precisely, thumbnails of pictures) in the assets folder.

With the folder and file name sorted out. Now let's deal with validation and loading. Let's start with the form. Let's do this:

 <?php echo $modMyModel->id_image ?> <?php echo CHtml::activeFileField($modMyModel, 'id_image_file'); ?> 


So we will see if the file is loaded. And the file itself will be loaded with the name id_image_file . Instead of echo $modMyModel->id_image you can insert a link to download the file or a thumbnail image.

Now validation. The idea is this: the validator should check if there is a file in $_FILES with the name id_image_file . If so, create a CUploadedFile object and write it to $modMyModel->id_image . Then validate $modMyModel->id_image standard way. To do this, create your own validator DFileValidator , inherited from CFileValidator . And immediately another one - DImageValidator , inherited from DFileValidator , in which we specify the default file types for images.

And finally, loading the file and saving the model. After validation, the uploaded file will be in $modMyModel->id_image , moreover in the form of CUploadedFile . In order for the download not to be tied to a specific property, you need to check before saving the model whether any of its properties are objects of the CUploadedFile class, and if they are, load them and save the addresses. Now the model will look like this:

 class MyModel extends DActiveRecord { public $id_image; public function rules(){ return array( array('id_image', 'DImageValidator'), ); } public function beforeSave() { foreach ($this->attributes as $key => $attribute) if ($attribute instanceof CUploadedFile) { $strSource = uniqid(); if ($attribute->saveAs(Yii::getPathOfAlias('application.data.files') . '/' . $strSource)) $this->$key = $strSource; } return parent::beforeSave(); } } 


The image property has been replaced by the id_image property consciously. Further it will be clear why.

Let's summarize

  1. All files are saved in the folder .../protected/data/files with random names.
  2. File loading is performed in the model, before being saved in the database.
  3. To mark a property as a file, you need:
    1. In the rules() model, assign a validator DFileValidator to this property.
    2. In the form, rename the input for this property by adding a '_file' to it.

  4. The actionCreate() and actionUpdate() methods can be left unchanged.


Second iteration We connect the database.


We figured out how to upload a file. But it is not clear how to contact him. What to write in the src tag parameter img ? How to give a file to download? In my opinion, it would be convenient to use the functionality of the Yii model for working with files. In fact, if a model is matched to each downloaded file, all low-level operations, including downloading, can be assigned to it. And in the $modMyModel->id_image store the primary key of this model (now the essence of the name of this property is clear). Then for $modMyModel it will be possible to determine the corresponding links and write, for example, like this:

 //  MyModel: public function relations() { return array( 'image' => array(self::BELONGS_TO, 'DImage', 'id_image'), ); } //  : $modMyModel = new MyModel; echo $modMyModel->id_image->image($htmlOptions); //        img echo $modMyModel->file->downloadLink(); //      


In addition, when using the model, the question of storing the original file name (which is lost when saving) is solved by itself.

Go.

Create a tbl_files table with the fields id, source, name . Define the DFile model associated with this table. In it we define a static upload method:

 class DFile extends DActiveRecord { public $uploadPath; //     public function init() { $this->uploadPath = Yii::getPathOfAlias('application.data.files'); } public static function upload($objFile) { $modFile = new DFile; $modFile->name = $objFile->name; $modFile->source = uniqid(); if ($objFile->saveAs($modFile->uploadPath . '/' . $modFile->source)) if ($modFile->save()) return $modFile; return null; } } 


And immediately create an empty class DImage extends DFile . We will need it later.
Now let's change our model a bit. Let's define the promised connection with the picture and slightly correct the beforeSave() method:

 class MyModel extends DActiveRecord { public $id_image; public function rules(){ return array( array('id_image', 'DImageValidator', 'allowEmpty' => true), ); } public function relations() { return array( 'image' => array(self::BELONGS_TO, 'DImage', 'id_image'), ); } public function beforeSave() { foreach ($this->attributes as $key => $attribute) if ($attribute instanceof CUploadedFile) { $modFile = DFile::upload($attribute); //   DFile $this->$key = $modFile->id; } return parent::beforeSave(); } } 


In the model's $modMyModel object, you can access the file via $modMyModel->image . How profitable it is to use - read on.

Third iteration. Appeals to downloaded files.


Up to this point, we almost did not share files and images. In fact, their loading is absolutely identical. The only difference is check before loading. But validators of DFileValidator and DImageValidator will perfectly cope with this, in which you can specify all the necessary rules.

In contrast to the download, access to the downloaded files and images are carried out in different ways. Files are loaded so that they can be downloaded later, and the pictures - so that they can be watched. Let's start with the files.

Work with files

Again, the files are downloaded in order to download them. This often requires verification of access rights. All you need to solve two problems, namely - the provision of links to download and, in fact, the issuance of a file for download.

Link generation is convenient to do in DFile . Like that:

 class DFile extends DActiveRecord { public function downloadLink($htmlOptions = array()) { return CHtml::link($this->name, array('/files/file/download', 'id' => $this->id), $htmlOptions); } } 


The link points to the FileController controller. We define it:

 class FileController extends DcController { public function actionDownload($id) { $modFile = $this->loadModel($id); header("Content-Type: application/force-download"); header("Content-Type: application/octet-stream"); header("Content-Type: application/download"); header("Content-Disposition: attachment; filename=" . $modFile->name); header("Content-Transfer-Encoding: binary "); readfile($modFile->uploadPath . '/' . $modFile->source); } public function loadModel($id) { $modFile = DFile::model()->findByPk($id); if($modFile === null) throw new CHttpException(404,'The requested page does not exist.'); return $modFile; } } 


I think everything is clear here. The actionDownload() method does not make any additional checks, but you can easily enable them if necessary. In the model now, having defined the appropriate link, you can write $modMyModel->file->downloadLink() . Of course, this approach will be less productive than issuing direct links to files. If performance is critical, you can order files not in a protected data directory, but in another (open) directory, and give direct links.

Work with pictures

With pictures, the situation is a bit more complicated. Images require the creation of thumbnails. In addition, with pictures, we certainly can not afford to produce dynamic links. Fortunately, Yii provides a convenient resource publishing mechanism that can be used for our purposes. The idea is this: we will create and publish miniatures as resources when generating links to a picture. Here, however, there are a couple of troubles. First, if you need access to the original picture, you will also have to copy it to the assets folder. Secondly, the publication will not be possible to implement the standard means of Yii. The fact is that for each published file, Yii will create its own folder, which will be brute force. Yes, and the creation of thumbnails immediately in the folder of assets using standard tools will not work.

The first problem may not be relevant if there are not very many images being downloaded and access to the original image is not required. The original images are stored in good resolution, they can be downloaded using the mechanism described above, and only thumbnails are used for display on the screen. If such a problem occurs, then, alternatively, you can not copy the original image to the assets folder, but create a link (the standard publishing mechanism in Yii suggests publishing with link creation).

As for the second problem, you will have to write the publication yourself. However, not too much to write ...

So let's go. We already have the DImage class inherited from DFile . We describe the creation of thumbnails and publication:

 class DImage extends DFile { public $assetsPath; //      public $assetsUrl; // URL    public $thumbs = array( 'min' => array('width' => 150, 'height' => 150), 'mid' => array('width' => 250), 'big' => array('width' => 600), ); //   public function init() { $this->assetsUrl = Yii::app()->assetManager->baseUrl . '/files'; $this->assetsPath = Yii::app()->assetManager->basePath . '/files'; if (!is_dir($this->assetsPath)) mkdir($this->assetsPath); } //      $this->assetsPath public function getIsPublished() { foreach ($this->thumbs as $kThumb => $vThumb) if (!is_file($this->assetsPath . '/' . $this->source . '_' . $kThumb)) return false; return true; } //   public function publish() { if (!$this->isPublished) foreach ($this->thumbs as $kThumb => $vThumb) $this->createThumb($this->uploadPath . '/' . $this->source, $this->assetsPath . '/' . $this->source . '_' . $kThumb, $kThumb); return $this->assetsUrl . '/' . $this->source; } //   function createThumb($strSrcFile, $strDstFile, $strThumb) { //    $strSrcFile,   $strDstFile } } 


To publish thumbnails, I suggest creating a subfolder of files in the assets folder. Considering that it is recommended to periodically clean the assets/files folder, it is necessary to check the existence of the assets/files folder every time. And create if needed. The name of the thumbnail is equal to the name of the image, supplemented by the identifier of the thumbnail. The image is considered published if all the miniatures are in place. There is no sense in checking the date of the original and published files, since the uploaded file cannot be changed. The publish() function returns the URL of the published image (but without a thumbnail), which does not contradict the idea of ​​publishing resources in Yii.

And finally, consider the appeal to the uploaded picture. DImage add the DImage class DImage the image() method:

 public function image($strThumb = 'min', $htmlOptions = array()) { return CHtml::image($this->publish() . '_' . $strThumb, $this->name, $htmlOptions); } 


Now, by analogy with files, you can write $modMyModel->image->image() in the model. By the way, if the size of the thumbnails suddenly needs to be changed or added a new one (it happened to me once), and all the files have been uploaded, it will be enough to change the size in the settings and clear the assets folder.

Finishing touches


Everything is working. Pictures are loaded, displayed. Files uploaded and downloaded. It remains to comb a little code. For example, the beforeSave () method can be removed from the MyModel class to the DActiveRecord class, from which, as you have noticed, all models are inherited. In addition, the display of inputs for files and images can be transferred to the class DActiveForm extends CActiveForm . Storage of settings can be assigned to the files module.

And, according to a good tradition, a link to download a working project . Laying out individual files turned out to be problematic due to the large number of dependencies, so I post the whole project. The database dump is protected / data / dump.sql. From the settings - specify the path to Yii, set access to the database. Base classes and validators are in the protected / components folder, all that concerns files is in the files module.

Conclusion


So, this is what we have at the exit:

  1. Centralized file loading and storage management
  2. Convenient interface for creating file properties in models
  3. High-level generation of links to download IMG files and tags


The idea can be developed. For example, almost all WYCIWYG editors offer an interface for downloading files and images. To do this, you only need to specify the download address. A download handler can be enabled in the FileController controller. But how then publish miniatures?

Or else, you can use the suggestions described in the articles Secure upload images to the server. Part one and Secure upload images to the server. Part Two . You can supplement the above mentioned upload extension. You can transfer the functional behavior.

In a word, it is too early to consider the proposed solution. But if the described idea turns out to be useful, he is ready to complete the work and publish the corresponding extension. Thanks to everyone who read it!

Source: https://habr.com/ru/post/156293/


All Articles