V
V
Vermut7562017-02-02 16:03:28
Yii
Vermut756, 2017-02-02 16:03:28

File upload and ActiveRecord - how? Namely, how to do saveAs of the file first, then model->save?

There is a standard task - a table with fields, one of the fields contains a file. Rather, it will contain the file name, and the file itself will be in a special folder. This solution is also quite standard.
So, I have an ActiveRecord successor model, I have a standard generated CRUD controller, everything works, but I need to add this very file.
What I do:
1. In the model, I declare the field itself (it’s as if it were a regular Model, by the way, for a long time I couldn’t understand why it says that it can’t getProperty, my attention was diverted by all these rules and fields and for some reason I began to think that if you write there, then ActiveRecord itself will create this field in the class, but in fact it creates them only for table columns)
public $myfile_logo;
2. Then I add validation rules for the field in the model:

public function rules()
    {
        return [
            ...
            [['myfile_logo'], 'file', 'skipOnEmpty' => false, 'extensions' => 'png, jpg'],
            ...
        ];
    }

3. Then in it I declare the following method:
public function uploadMyFiles() {
    
    //just gets it from $_FILES
            $this->myfile_logo = UploadedFile::getInstance($this, 'myfile_logo');
    
    //TODO this->validate() should be here,
    // or all this should be in save() and validate should be in save
    
    //$this->validate(['myfile_logo']); hasn't sense because ActiveRecord anyway validates all
    
    //just move_uploaded_file
    $newFileId = $this->myfile_logo->baseName . '_' . time() . '_' . uniqid();
    $newFileName = $newFileId . '.png'; //TODO do not stupidly cast any extension to png
    $saveRes = $this->myfile_logo->saveAs('files/images/' . $newFileName);
    
    if ($saveRes){
      $this->img_logo = $newFileId;
      
      return true;
    } else {
      return false;
    }
  }

4. Further, as you might guess, in the controller, I first call this same uploadMyFiles function from the model so that it moves the file to a permanent folder, and inserts the resulting file name into img_logo (this is already a database field), immediately after that, in the same place in the controller, call save() to save the model to the database along with this img_logo. In short, just like this:
$model = new Fruit();

if ($model->load(Yii::$app->request->post()) && $model->uploadMyFiles() && $model->save()) {
...

But my plans are ruined by the validator - after all, after I called uploadMyFiles, I call save, it calls ActiveRecord::insert() inside, and it’s Model::validate() which tries to check a temporary file, but the temporary file is already no.
I tried to fix the situation by adding selective file validation to uploadMyFiles, which, as I correctly noted in TODO, is exactly the place:
$this->validate(['myfile_logo']);
In the code above, this line is commented out.
But, as it turned out, this trick does not work - Model::validate() from ActiveRecord::insert() still stupidly validates all fields, including this one, which has already been checked in general.
For the time being, I "solved" the problem temporarily by simply removing the file validation altogether, that is, by commenting out the rule in rules().
But this is not a solution.
So the question is how to make it more logical?
I foresee that some (many) believe that here it is necessary to abandon ActiveRecord altogether, wrapping it in a regular model and working with the file in it, and not in ActiveRecord.
However, judging by Gii, the authors of the framework recommend direct work with ActiveRecord, and if you still have to wrap it up on a real project, then why doesn't Gii do this automatically?
PS I do not want to dig ready-made libraries like kartik. I don’t really like them - they are crooked, you don’t know how much you have to redo, sometimes it’s even easier to write your own. Unless you look for how the problem is solved there - they seem to work fine with ActiveRecord, there are examples.

Answer the question

In order to leave comments, you need to log in

[[+comments_count]] answer(s)
D
Dmitry, 2017-02-02
@slo_nik

Good afternoon. Everything is
in the documentation . p.s. I used mihaildev\elfinder\InputFile to upload the file, a perfectly acceptable solution. Connects elementarily

<?= $form->field($model, 'logo')->widget(InputFile::className(),['path' => 'logo_dir', 'options' => ['readonly' => true]]) ?>

T
tkutru, 2017-02-02
@tkutru

There are three main options.
1. Correct: Respect the Single Responsibility Principle.
Those. if there is some kind of model and you can additionally attach pictures to it, then create a separate class for the upload form, in its properties you can separately specify a set of properties for the model and separately upload files.
Actually, in the form validation rules, check files for compliance with formats. When saving the form, validate-save the file(s) separately and the model separately.
www.yiiframework.com/doc-2.0/guide-input-file-uplo...
2. Less correct: fix a bug in the current solution.
The problem is that your uploadMyFiles method is called before the validator. Accordingly, you can call it not in the controller, but in the afterValidate method of the model (it will be called before saving the model and after passing all validations).
3. Other methods (least preferred).
In the file validator, you can set a scenario in which it (not) will work, after loading - change the scenario to one in which the validator does not work. Another option is to use ready-made solutions. Another option is to replace the validator with a custom one that will check the file for format compliance and trigger a subsequent download.

Didn't find what you were looking for?

Ask your question

Ask a Question

731 491 924 answers to any question