CakePHP 3 upload plików

W tej części zajmiemy się implementacją uploadu plików. Utwórzmy w bazie danych tabelę przechowującą pliki. Jest ona powiązana z modelem Applications oraz z Users.

CREATE TABLE `documents` (
  `id` bigint(11) NOT NULL,
  `application_id` bigint(11) NOT NULL,
  `user_id` bigint(11) NOT NULL,
  `name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
  `filename` varchar(125) COLLATE utf8mb4_unicode_ci NOT NULL,
  `description` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `modified` datetime NOT NULL,
  `created` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
ALTER TABLE `documents` ADD PRIMARY KEY (`id`);
Teraz utworzmy pliki modelu najpierw src/Model/Entity/Document.php.
namespace App\Model\Entity;

use Cake\ORM\Entity;

class Document extends Entity
{
    protected $_accessible = [
        '*' => true,
        'id' => false
    ];
}
Potem src/Model/Table/DocumentsTable.php.
namespace App\Model\Table;

use Cake\ORM\Query;
use Cake\ORM\RulesChecker;
use Cake\ORM\Table;
use Cake\Validation\Validator;

class DocumentsTable extends Table
{
    public function initialize(array $config)
    {
        parent::initialize($config);

        $this->table('documents');
        $this->displayField('name');
        $this->primaryKey('id');

        $this->addBehavior('Timestamp');

        $this->belongsTo('Applications');
        $this->belongsTo('Users');
    }

    public function validationDefault(Validator $validator)
    {
        $validator
            ->allowEmpty('id', 'create');

        $validator
            ->requirePresence('name', 'create')
            ->notEmpty('name');

        $validator
            ->requirePresence('filename', 'create')
            ->notEmpty('filename');

        return $validator;
    }

    public function buildRules(RulesChecker $rules)
    {
        $rules->add($rules->existsIn(['application_id'], 'Applications'));
        $rules->add($rules->existsIn(['user_id'], 'Users'));

        return $rules;
    }
}
Nasze dokumenty są powiązane z użytkownikami i aplikacjami więc musimy zaktualizować ich klasy Table src/Model/Table/UsersTable.php oraz src/Model/Table/ApplicationsTable.php. Dodajemy poniższą linię w funkcji initialize.
$this->hasMany('Documents');
Podczas ładowania Aplikacji lub Użytkownika musimy koniecznie załączyć powiązane Dokumenty za pomocą contain. W kontrolerach zaktualizuj funkcję view. W pliku src/Controller/ApplicationsController.php:
$application = $this->Applications->get($id, ['contain' => 'Documents']);
W pliku src/Controller/UsersController.php:
$user = $this->Users->get($id, ['contain' => 'Documents']);
Dodamy dokumenty z poziomu widoku Aplikacji, a także listę powiązanych dokumentów, edytując widok src/Template/Applications/view.ctp i dodając na końcu tego pliku:
<div class="related">
	<?php echo $this->Html->link(__('New Document'), ['controller' => 'Documents', 'action' => 'add', $application->id], ['class' => 'right']) ?>
	<h3><?php echo __('Documents') ?></h3>
	<?php if (!empty($application->documents)): ?>
	<table cellpadding="0" cellspacing="0">
		<tr>
			<th scope="col"><?php echo __('Name') ?>
			<th scope="col"><?php echo __('Description') ?>
			<th scope="col" class="actions"><?php echo __('Actions') ?>
		</tr>
		<?php foreach ($application->documents as $documents): ?>
		<tr>
			<td><?php echo $this->Html->link($documents->name, '/files/' . $documents->filename) ?>
			<td><?php echo h($documents->description) ?></td>
			<td class="actions">
				<?php echo $this->Html->link(__('View'), '/files/' . $documents->filename) ?> |
				<?php echo $this->Html->link(__('Edit'), ['controller' => 'Documents', 'action' => 'edit', $documents->id]) ?>
			</td>
		</tr>
		<?php endforeach; ?>
	</table>
	<?php endif; ?>
</div>
Dodamy teraz widok dodawania dokumentu (src/Template/Documents/add.ctp).
<div class="documents form large-9 medium-8 columns content">
    <?php echo $this->Form->create($document, ['type' => 'file']) ?>
    <fieldset>
        <legend><?php echo __('Add Document') ?></legend>
        <?php
            echo $this->Form->input('application', ['type' => 'text', 'default' => $application_id, 'disabled' => true]);
            echo $this->Form->input('name', ['autofocus' => 'true']);
            echo $this->Form->input('file', ['type' => 'file', 'required' => true]);
            echo $this->Form->input('description');
            echo $this->Form->input('application_id', ['type' => 'hidden', 'value' => $application_id]);
            echo $this->Form->input('user_id', ['type' => 'hidden', 'value' => $user_id]);
        ?>
    </fieldset>
    <?php echo $this->Form->button(__('Submit')) ?>
    <?php echo $this->Html->link(__('Cancel'), ['controller' => 'Applications', 'action' => 'view', $application_id], ['class' => 'button']); ?>
    <?php echo $this->Form->end() ?>
</div>
Widok edycji dokumentu (src/Template/Documents/edit.ctp).
<div class="documents form large-9 medium-8 columns content">
    <?php echo $this->Form->create($document, ['type' => 'file']) ?>
    <fieldset>
        <legend><?php echo __('Edit Document') ?></legend>
        <?php echo $this->Form->input('name'); ?>
        <div><strong>Current File</strong></div>
        <?php
            echo $this->Html->link($document->filename, '/files/' . $document->filename);
            echo $this->Form->input('file', ['type' => 'file']);
            echo $this->Form->input('description');
        ?>
    </fieldset>
    <?php echo $this->Form->button(__('Submit')) ?>
    <?php echo $this->Html->link(__('Cancel'), ['controller' => 'Applications', 'action' => 'view', $document->application_id], ['class' => 'button']); ?>
    <?php echo $this->Form->end() ?>
</div>
Przejdźmy teraz do kontrolera dokumentów. Tworzymy następujący plik (src/Controller/DocumentsController.php).
namespace App\Controller;

use App\Controller\AppController;

class DocumentsController extends AppController
{
    public function isAuthorized($user) {
        if ($user['role'] == 'Admin') { // Only Admin can add/edit documents
            return true;
        }
        return false;
    }

    public function add($application_id = null) {
        if (is_null($application_id)) { // Documents must be attached to an application
            $this->redirect(['controller' => 'Applications', 'action' => 'index']);
        }
        $document = $this->Documents->newEntity();
        if ($this->request->is('post')) {
            $file = $this->request->data['file'];
            $file['name'] =  time() . '-' . str_replace(' ', '_', $file['name']); // timestamp files to prevent clobber
            if (move_uploaded_file($file['tmp_name'], WWW_ROOT . 'files/' . $file['name'])) {
                $this->request->data['filename'] = $file['name'];
                $document = $this->Documents->patchEntity($document, $this->request->data);
                if ($this->Documents->save($document)) {
                    $this->Flash->success(__('The document has been saved.'));
                    return $this->redirect(['controller' => 'Applications', 'action' => 'view', $document->application_id]);
                } else {
                    $this->Flash->error(__('The document could not be saved. Please, try again.'));
                }
            } else {
                $this->Flash->error(__('Could not upload the file'));
            }
        }
        $user_id = $this->Auth->user('id');
        $this->set(compact('document', 'application_id', 'user_id'));
        $this->set('_serialize', ['document']);
    }

    public function edit($id = null) {
        $document = $this->Documents->get($id);
        if ($this->request->is(['patch', 'post', 'put'])) {
            $file = $this->request->data['file'];
            if ($file['name'] != '' && move_uploaded_file($file['tmp_name'], WWW_ROOT . 'files/' . $file['name'])) {
                $this->request->data['filename'] = $file['name'];
            }
            $document = $this->Documents->patchEntity($document, $this->request->data);
            if ($this->Documents->save($document)) {
                $this->Flash->success(__('The document has been saved.'));
                return $this->redirect(['controller' => 'Applications', 'action' => 'view', $document->application_id]);
            } else {
                $this->Flash->error(__('The document could not be saved. Please, try again.'));
            }
        }
        $this->set(compact('document'));
        $this->set('_serialize', ['document']);
    }
}
Ponieważ zapisujemy pliki do katalogu /webroot/files/ naszej aplikacji, upewnij się, że katalog ten istnieje i że użytkownik (np. Apache) ma prawo zapisu do tego katalogu.