{{< minver v="1.7.7" title="true" >}}
Введение
В этом руководстве мы создадим модуль, который расширяет форму Поставщиков
(ПРОДАЖИ -> Каталог -> Бренды и Поставщики). Этот модуль решит следующую задачу
«Я хочу добавить поле 'загрузка изображения' в форму добавления/редактирования поставщика, потому что хочу
отображать новый логотип для каждого поставщика в моем магазине. Таким образом, это новое поле должно позволять мне добавлять,
редактировать и удалять файлы изображений, связанные с поставщиком. Очевидно, что я ожидаю, что загруженные файлы будут
сохранены в соответствии с тем, как PrestaShop обычно сохраняет такие файлы».
Мы создадим модуль для решения этой задачи, используя хуки PrestaShop на странице добавления/редактирования поставщика в панели администратора, и будем следовать некоторым лучшим практикам разработки программного обеспечения, таким как принципы SOLID, чтобы сделать наш код как можно более поддерживаемым!
Вы научитесь создавать:
- Главный класс модуля: основной входной и крючковой точки модуля
- Класс установщика: ответственный за процесс установки и удаления модуля
- Создание контроллера Symfony: необходимо, так как мы добавляем новое действие контроллера «удалить изображение»
- Сущность Doctrine: эта модель отвечает за сохранение данных изображения
- Класс репозитория: эта модель предназначена для поиска и извлечения изображений из базы данных
- Класс загрузчика изображений: этот класс отвечает за процесс загрузки изображений
- Шаблон представления Twig: необходим для отображения
Главный класс модуля
Создадим главный класс модуля DemoExtendSymfonyForm2
<?php
// так как этот модуль совместим с PS 1.7.7 и позже, мы
// можем использовать строгие типы PHP7, так как поддержка PHP5 была прекращена для PS 1.7.7
declare(strict_types=1);
// use statements
if (!defined('_PS_VERSION_')) {
exit;
}
// необходим для использования Composer для автозагрузки этого модуля
require_once __DIR__.'/vendor/autoload.php';
/**
* Класс demoextendsymfonyform
*/
class DemoExtendSymfonyForm2 extends Module
{
private const SUPPLIER_EXTRA_IMAGE_PATH = '/img/su/';
public function __construct()
{
$this->name = 'demoextendsymfonyform2';
$this->author = 'PrestaShop';
$this->version = '1.0.0';
$this->ps_versions_compliancy = ['min' => '1.7.7.0', 'max' => _PS_VERSION_];
parent::__construct();
$this->displayName = $this->l('Демонстрация форм Symfony #2');
$this->description = $this->l(
'Демонстрация того, как добавить поле загрузки изображения в форму Symfony'
);
}
}
Создадим класс установщика, ответственного за регистрацию хуков и управление базой данных:
<?php
declare(strict_types=1);
namespace PrestaShop\Module\DemoExtendSymfonyForm\Install;
use Db;
use Module;
/**
* Класс Установщика
* @package PrestaShop\Module\DemoExtendSymfonyForm\Install
*/
class Installer
{
/**
* Входная точка установки модуля.
*
* @param Module $module
*
* @return bool
*/
public function install(Module $module): bool
{
if (!$this->registerHooks($module)) {
return false;
}
if (!$this->installDatabase()) {
return false;
}
return true;
}
/**
* Входная точка удаления модуля.
*
* @return bool
*/
public function uninstall(): bool
{
return $this->uninstallDatabase();
}
/**
* Установить изменения в базе данных, необходимые для этого модуля.
*
* @return bool
*/
private function installDatabase(): bool
{
$queries = [
'CREATE TABLE IF NOT EXISTS `'._DB_PREFIX_.'supplier_extra_image` (
`id_extra_image` int(11) NOT NULL AUTO_INCREMENT,
`id_supplier` int(11) NOT NULL,
`image_name` varchar(64) NOT NULL,
PRIMARY KEY (`id_extra_image`)
) ENGINE='._MYSQL_ENGINE_.' DEFAULT CHARSET=utf8;',
];
return $this->executeQueries($queries);
}
/**
* Удалить изменения в базе данных.
*
* @return bool
*/
private function uninstallDatabase(): bool
{
$queries = [
'DROP TABLE IF EXISTS `'._DB_PREFIX_.'supplier_extra_image`',
];
return $this->executeQueries($queries);
}
/**
* Зарегистрировать хуки для модуля.
*
* @param Module $module
*
* @return bool
*/
private function registerHooks(Module $module): bool
{
$hooks = [
'actionSupplierFormBuilderModifier',
'actionAfterCreateSupplierFormHandler',
'actionAfterUpdateSupplierFormHandler',
];
return (bool) $module->registerHook($hooks);
}
/**
* Вспомогательный метод для выполнения нескольких запросов к базе данных.
*
* @param array $queries
*
* @return bool
*/
private function executeQueries(array $queries): bool
{
foreach ($queries as $query) {
if (!Db::getInstance()->execute($query)) {
return false;
}
}
return true;
}
}
Используем класс Installer
в главном классе модуля, добавив ниже указанный код в класс DemoExtendSymfonyForm2
.
<?php
use PrestaShop\Module\DemoExtendSymfonyForm\Install\Installer;
[...]
/**
* @return bool
*/
public function install()
{
if (!parent::install()) {
return false;
}
$installer = new Installer();
return $installer->install($this);
}
/**
* @return bool
*/
public function uninstall()
{
$installer = new Installer();
return $installer->uninstall() && parent::uninstall();
}
Создадим класс сущности SupplierExtraImage
. Мы используем [Doctrine]({{< relref "/8/modules/concepts/doctrine/" >}}), который доступен для модулей PrestaShop с версии 1.7.6.
<?php
declare(strict_types=1);
namespace PrestaShop\Module\DemoExtendSymfonyForm\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Table()
* @ORM\Entity(repositoryClass="PrestaShop\Module\DemoExtendSymfonyForm\Repository\SupplierExtraImageRepository")
*/
class SupplierExtraImage
{
/**
* @var int
*
* @ORM\Id
* @ORM\Column(name="id_extra_image", type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @ORM\Column(name="id_supplier", type="integer")
*/
private $supplierId;
/**
* @var string
*
* @ORM\Column(type="string")
*/
private $imageName;
/**
* @return int
*/
public function getId(): int
{
return $this->id;
}
/**
* @param int $id
*/
public function setId(int $id): void
{
$this->id = $id;
}
/**
* @return mixed
*/
public function getSupplierId()
{
return $this->supplierId;
}
/**
* @param mixed $supplierId
*/
public function setSupplierId($supplierId): void
{
$this->supplierId = $supplierId;
}
/**
* @return string
*/
public function getImageName(): string
{
return $this->imageName;
}
/**
* @param string $imageName
*/
public function setImageName(string $imageName): void
{
$this->imageName = $imageName;
}
}
Создадим класс SupplierExtraImageRepository
:
<?php
declare(strict_types=1);
namespace PrestaShop\Module\DemoExtendSymfonyForm\Repository;
use Doctrine\ORM\EntityRepository;
use PrestaShop\Module\DemoExtendSymfonyForm\Entity\SupplierExtraImage;
/**
* Класс SupplierExtraImageRepository
* @package PrestaShop\Module\DemoExtendSymfonyForm\Repository
*/
class SupplierExtraImageRepository extends EntityRepository
{
/**
* @param $supplierId
* @param $imageName
*/
public function upsertSupplierImageName($supplierId, $imageName)
{
/** @var SupplierExtraImage $supplierExtraImage */
$supplierExtraImage = $this->findOneBy(['supplierId' => $supplierId]);
if (!$supplierExtraImage) {
$supplierExtraImage = new SupplierExtraImage();
$supplierExtraImage->setSupplierId($supplierId);
}
$supplierExtraImage->setImageName($imageName);
$em = $this->getEntityManager();
$em->persist($supplierExtraImage);
$em->flush();
}
/**
* @param SupplierExtraImage $supplierExtraImage
*/
public function deleteExtraImage(SupplierExtraImage $supplierExtraImage)
{
$em = $this->getEntityManager();
if ($supplierExtraImage) {
$em->remove($supplierExtraImage);
$em->flush();
}
}
}
Создадим функцию хука hookActionSupplierFormBuilderModifier
в главном классе модуля.
Этот хук доступен для [CRUD форм]({{< relref "/8/modules/sample-modules/grid-and-identifiable-object-form-hooks-usage" >}}) на страницах Symfony в PrestaShop.
<?php
/**
* @param array $params
*/
public function hookActionSupplierFormBuilderModifier(array $params)
{
/** @var SupplierExtraImageRepository $supplierExtraImageRepository */
$supplierExtraImageRepository = $this->get(
'prestashop.module.demoextendsymfonyform.repository.supplier_extra_image_repository'
);
$translator = $this->getTranslator();
/** @var FormBuilderInterface $formBuilder */
$formBuilder = $params['form_builder'];
// мы добавляем в форму Symfony поле `upload_image_file`, которое будет использоваться пользователем BO для загрузки файлов изображений
$formBuilder
->add('upload_image_file', FileType::class, [
'label' => $translator->trans('Загрузить файл изображения', [], 'Modules.DemoExtendSymfonyForm'),
'required' => false,
]);
/** @var SupplierExtraImage $supplierExtraImage */
$supplierExtraImage = $supplierExtraImageRepository->findOneBy(['supplierId' => $params['id']]);
if ($supplierExtraImage && file_exists(_PS_SUPP_IMG_DIR_ . $supplierExtraImage->getImageName())) {
// Когда для этого поставщика уже зарегистрировано изображение, мы добавляем в форму Symfony
// 'image_file', чтобы предоставить пользователю BO предварительный просмотр и также предоставить кнопку "удалить"
$formBuilder
->add('image_file', CustomContentType::class, [
'required' => false,
'template' => '@Modules/demoextendsymfonyform2/src/View/upload_image.html.twig',
'data' => [
'supplierId' => $params['id'],
'imageUrl' => self::SUPPLIER_EXTRA_IMAGE_PATH . $supplierExtraImage->getImageName(),
],
]);
}
}
Создадим класс SupplierExtraImageUploader
:
<?php
declare(strict_types=1);
namespace PrestaShop\Module\DemoExtendSymfonyForm\Uploader;
use PrestaShop\Module\DemoExtendSymfonyForm\Entity\SupplierExtraImage;
use PrestaShop\Module\DemoExtendSymfonyForm\Repository\SupplierExtraImageRepository;
use PrestaShop\PrestaShop\Core\Image\Uploader\Exception\ImageOptimizationException;
use PrestaShop\PrestaShop\Core\Image\Uploader\Exception\ImageUploadException;
use PrestaShop\PrestaShop\Core\Image\Uploader\Exception\MemoryLimitException;
use PrestaShop\PrestaShop\Core\Image\Uploader\Exception\UploadedImageConstraintException;
use PrestaShop\PrestaShop\Core\Image\Uploader\ImageUploaderInterface;
use Symfony\Component\HttpFoundation\File\UploadedFile;
/**
* Класс SupplierExtraImageUploader
* @package PrestaShop\Module\DemoExtendSymfonyForm\Uploader
*/
class SupplierExtraImageUploader implements ImageUploaderInterface
{
/** @var SupplierExtraImageRepository */
private $supplierExtraImageRepository;
/**
* @param SupplierExtraImageRepository $supplierExtraImageRepository
*/
public function __construct(SupplierExtraImageRepository $supplierExtraImageRepository)
{
$this->supplierExtraImageRepository = $supplierExtraImageRepository;
}
/**
* @param int $supplierId
* @param UploadedFile $image
*/
public function upload($supplierId, UploadedFile $image)
{
$this->checkImageIsAllowedForUpload($image);
$tempImageName = $this->createTemporaryImage($image);
$this->deleteOldImage($supplierId);
$originalImageName = $image->getClientOriginalName();
$destination = _PS_SUPP_IMG_DIR_ . $originalImageName;
$this->uploadFromTemp($tempImageName, $destination);
$this->supplierExtraImageRepository->upsertSupplierImageName($supplierId, $originalImageName);
}
/**
* Создает временное изображение из загруженного файла
*
* @param UploadedFile $image
*
* @throws ImageUploadException
*
* @return string
*/
protected function createTemporaryImage(UploadedFile $image)
{
$temporaryImageName = tempnam(_PS_TMP_IMG_DIR_, 'PS');
if (!$temporaryImageName || !move_uploaded_file($image->getPathname(), $temporaryImageName)) {
throw new ImageUploadException('Не удалось создать временный файл изображения');
}
return $temporaryImageName;
}
/**
* Загружает измененное изображение из временной папки в конечную папку изображения
*
* @param $temporaryImageName
* @param $destination
*
* @throws ImageOptimizationException
* @throws MemoryLimitException
*/
protected function uploadFromTemp($temporaryImageName, $destination)
{
if (!\ImageManager::checkImageMemoryLimit($temporaryImageName)) {
throw new MemoryLimitException('Невозможно загрузить изображение из-за ограничений памяти');
}
if (!\ImageManager::resize($temporaryImageName, $destination)) {
throw new ImageOptimizationException(
'Произошла ошибка при загрузке изображения. Проверьте права доступа к вашей директории.'
);
}
unlink($temporaryImageName);
}
/**
* Удаляет старое изображение
*
* @param $supplierId
*/
private function deleteOldImage($supplierId)
{
/** @var SupplierExtraImage $supplierExtraImage */
$supplierExtraImage = $this->supplierExtraImageRepository->findOneBy(['supplierId' => $supplierId]);
if ($supplierExtraImage && file_exists(_PS_SUPP_IMG_DIR_ . $supplierExtraImage->getImageName())) {
unlink(_PS_SUPP_IMG_DIR_ . $supplierExtraImage->getImageName());
}
}
/**
* Проверяет, разрешено ли изображение для загрузки.
*
* @param UploadedFile $image
*
* @throws UploadedImageConstraintException
*/
protected function checkImageIsAllowedForUpload(UploadedFile $image)
{
$maxFileSize = \Tools::getMaxUploadSize();
if ($maxFileSize > 0 && $image->getSize() > $maxFileSize) {
throw new UploadedImageConstraintException(
sprintf(
'Максимально допустимый размер файла "%s" байт. Размер загруженного изображения "%s".',
$maxFileSize, $image->getSize()
),
UploadedImageConstraintException::EXCEEDED_SIZE
);
}
if (!\ImageManager::isRealImage($image->getPathname(), $image->getClientMimeType())
|| !\ImageManager::isCorrectImageFileExt($image->getClientOriginalName())
|| preg_match('/\%00/', $image->getClientOriginalName()) // предотвращение внедрения нулевого байта
) {
throw new UploadedImageConstraintException(
sprintf(
'Формат изображения "%s" не распознан, допустимые форматы: .gif, .jpg, .png',
$image->getClientOriginalExtension()
),
UploadedImageConstraintException::UNRECOGNIZED_FORMAT
);
}
}
}
Создадим хук hookActionAfterUpdateSupplierFormHandler
в главном классе модуля:
<?php
/**
* @param array $params
*/
public function hookActionAfterUpdateSupplierFormHandler(array $params)
{
$this->uploadImage($params);
}
Создадим еще один хук hookActionAfterCreateSupplierFormHandler
в главном классе модуля:
<?php
/**
* @param array $params
*/
public function hookActionAfterCreateSupplierFormHandler(array $params)
{
$this->uploadImage($params);
}
Добавим функцию UploadImage
в главный класс:
<?php
/**
* @param array $params
*/
private function uploadImage(array $params): void
{
/** @var ImageUploaderInterface $supplierExtraImageUploader */
$supplierExtraImageUploader = $this->get(
'prestashop.module.demoextendsymfonyform.uploader.supplier_extra_image_uploader'
);
/** @var UploadedFile $uploadedFile */
$uploadedFile = $params['form_data']['upload_image_file'];
if ($uploadedFile instanceof UploadedFile) {
$supplierExtraImageUploader->upload($params['id'], $uploadedFile);
}
}
{{% notice note %}} Вы можете найти готовое решение в репозитории PrestaShop example-modules на GitHub: https://github.com/PrestaShop/example-modules/tree/master/demoextendsymfonyform2 {{% /notice %}}