Всем привет! Это первая моя статья, поэтому сильно не ругайте!
Столкнулся с задачей, в которой нужно реализовать отправку формы с прикрепленными файлами. При этом форма должна сохраняться в админке.

Для решения задачи были выбраны компоненты: Formit, AjaxForm.
Сразу скажу, что использую Fenom и все шаблоны, чанки, сниппеты и хуки у меня файловые. Благо Formit умеет работать с файловыми хуками.

В документации Formit сказано, что он умеет сохранять файлы во временную папку. За данную возможность отвечает параметр сниппета saveTmpFiles. Воспользуемся этим и укажем этот параметр в вызов сниппета.

Код вызова сниппета:
{var $moveFile = $_modx->config['formit.path_hooks'] ~ 'moveFile.php'}
{$_modx->runSnippet('!ajaxForm',[
    'form' => '@FILE chunks/form/feedbackForm.tpl',
    'hooks' => 'notSpam,FormItSaveForm,email,' ~ $moveFile,
    'formFields' => 'name,email,phone,message,file',
    'fieldNames' => 'name==Имя отправителя,phone==Телефон,email==Email,message==Комментарий отправителя,file==Файл',
    'formName' => 'Страница контакты',
    'emailTpl' => '@FILE chunks/letters/kontaktLeter.tpl',
    'emailTo' => 'email' | config,
    'emailFrom' => 'email_sender' | config,
    'emailFromName' => 'Сайт ООО Рога и Копыта',
    'emailReplyTo' => $_modx->getPlaceholder('email'),
    'emailSubject' => 'Письмо с сайта',
    'saveTmpFiles' => true,
    'validate' => 'name:required,email:required,message:required',
    'validationErrorMessage' => 'В форме содержатся ошибки!',
    'successMessage' => 'Сообщение успешно отправлено'
])}

Пояснение:
{var $moveFile = $_modx->config['formit.path_hooks'] ~ 'moveFile.php'}
— Тут я указываю путь и имя файлового хука, предварительно создав системную настройку formit.path_hooks, в которой собственно сам путь!
'hooks' => 'notSpam,FormItSaveForm,email,' ~ $moveFile,
— здесь указано 4 хука:
  1. notSpam — Мой не файловый хук для защиты от спама.
  2. FormItSaveForm — Стандартный хук сохранения формы.
  3. email — Стандартный хук отправки сообщения.
  4. $moveFile — переменная с файловым хуком. Он должен быть последним, иначе на почту не приходит прикрепленный файл.
'saveTmpFiles' => true,
— Просим сохранять файл во временную папку.
Остальное должно быть понятно. Если не понятно, то обратитесь к документации Formit, AjaxForm, Fenom, Парсер pdoTools.
Чанки с формой и письмом не содержат ничего необычного, поэтому останавличаться на них не будем.

Код хука я документировал для себя и не могу сказать что он хорошо задокументирован!))) Так что сори.
По сути это рабочая заготовка, которую можно и нужно развивать и усовершенствовать. Нужно учитывать, что этот хук умеет работать только с одним файлом.
<?php
//$modx->log(modX::LOG_LEVEL_ERROR, 'SendMessages. Message: '.print_r($hook->getValues(), true));

/**
 * Указываем путь для хранения прикрепленных изображений
 */
$formPath = $modx->getOption('assets_path') . 'uploads/forms/';
$formPathBase = $modx->getOption('assets_url') . 'uploads/forms/';
$pathFormFile = $formPath . $formData['id'] . '/';
$pathFormFileBase = $formPathBase . $formData['id'] . '/';

/**
 * Если не указан файл
 */
$formValues = $hook->getValues();
if (empty($formValues['file'])) {return true;}

/**
 * Если указанный файл не существует во временной директории
 */
$fileTmpName = $formValues['file']['tmp_name'];
if (!file_exists($fileTmpName)) {return true;}

/**
 * Получаем данные из присланной формы
 */
$fileName = $formValues['file']['name'];
$formData = $hook->getValue('savedForm');

/**
 * Получаем сохраненную форму
 */
$saveForm = $modx->getObject('FormItForm', array('id' => $formData['id']));
$valuesForm = $saveForm->get('values');
$valuesForm = $modx->fromJSON($valuesForm);

/**
 * Перемещение файла
 */
$flag = true;
if (!file_exists($pathFormFile)) {
    $flag = mkdir($pathFormFile, octdec ($modx->getOption('new_file_permissions')), true);
}
if (!$flag) {return true;}
$newFormFile = $pathFormFile . $fileName;
$newFormFileBase = $pathFormFileBase . $fileName;
if (!rename($fileTmpName, $newFormFile)) {return true;}

/**
 * Подмена значения для сохранения формы.
 * Указываем путь к файлу.
 */
$newSaveForm = array();
foreach($valuesForm as $key => $value) {
    if ($value == $fileName) {
        $value = '<a target="_blank" href="' . $newFormFileBase . '">' . $newFormFileBase . '</a>';
    }
    $newSaveForm[$key] = $value;
}

/**
 * Сохраняем измененную форму
 */
$saveForm->set('values', $modx->toJSON($newSaveForm));
if ($saveForm->save() === false) {
    $modx->log(modX::LOG_LEVEL_ERROR, 'SendMessages. Message: Ne zalilos\'');
}

return true;

Собственно даже не знаю что тут объяснять. На мой взгляд тут все проще некуда. Единственное на что я потратил много времени, так это на то, что бы понять как правильно указать права на создаваемую папку!

В результате в письме мы имеем прикрепленный файл, а в сохраненной форме — ссылку на файл, который открывается в новой вкладке. Конечно можно используя ExtJS сделать открытие файла в попап окне, но до этого я пока не добрался!

Надеюсь статья будет кому-то полезна!

P.S: К сожалению я не смог найти событие, на которое можно было бы повесить плагин для удаления файлов вместе с формой, поэтому контролировать данный момент придется руками. Ну или написать скриптик на основе API, который будет получать список каталогов (а название у них == id формы), смотреть на наличие формы и если формы с таким id нет, то удалять каталог.