<?php
namespace App\Form;
use App\Enum\Theme\Theme;
use App\Library\Utils\Dev\ReflectionUtils\ReflectionUtils;
use App\Library\Utils\Other\Other;
use App\Service\BaseEntityService;
use App\Service\RouteService;
use App\Service\ServiceRetriever;
use App\Service\ThemeService;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Validator\Constraints\File;
use Symfony\Component\Validator\Constraints\Image;
class AbstractBaseType extends AbstractType
{
/** @var RouterInterface */
private $router;
private $addFormFieldCallbacks = [];
protected $textFields = [];
protected $imageFields = [];
protected $textAreaFields = [];
/**
* @var ThemeService
*/
protected $themeService;
/**
* @var RouteService
*/
protected $routeService;
/**
* @var ServiceRetriever
*/
protected $serviceRetriever;
/**
* @var \App\Form\DataTransformer\DataTransformerRetriever
*/
protected $dataTransformerRetriever;
/** @var FormBuilderInterface */
protected $builder;
/**
* @var ReflectionUtils|null
*/
private $reflectionUtils;
public function __construct(RouterInterface $router, RouteService $routeService, ServiceRetriever $serviceRetriever)
{
$this->router = $router;
// $this->themeService = $themeService;
$this->routeService = $routeService;
$this->serviceRetriever = $serviceRetriever;
$this->dataTransformerRetriever = $this->serviceRetriever->getDataTransformerRetriever();
$this->reflectionUtils = $this->serviceRetriever->getService(ReflectionUtils::class);
}
public function getBuilder(): FormBuilderInterface
{
if (!$this->builder) {
throw new \Exception("Form builder is not set.");
}
return $this->builder;
}
public function setBuilder(FormBuilderInterface $builder): void
{
$this->builder = $builder;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$this->addTextFields($builder, $options);
foreach ($this->imageFields as $field) {
$this->addImageField($builder, $field);
}
foreach ($this->textAreaFields as $field) {
$builder->add($field, TextareaType::class, [
'required' => false
]);
}
$builder->addEventListener(FormEvents::PRE_SET_DATA,
function (FormEvent $event) {
$form = $event->getForm();
$item = $event->getData();
foreach ($this->addFormFieldCallbacks as $addFormFieldCallback) {
$addFormFieldCallback($form, $item);
}
}
);
}
protected function afterBuildForm(FormBuilderInterface $builder, array $options): void
{
if ($options['submitted_fields_only']) {
$this->handleSubmittedFieldsOnly($builder);
}
}
public function addEnumFormField(FormBuilderInterface $builder, string $name, string $enumClass,
array $allowedValues = null, array $choices = null, array $extraOptions = []) {
$reflection = new \ReflectionClass($enumClass);
if ($choices === null) {
$choices = [];
foreach ($reflection->getConstants() as $constName => $constValue) {
if ($allowedValues && !in_array($constValue, $allowedValues)) {
continue;
}
$choices[$enumClass::getText($constValue)] = $constValue;
}
}
$builder->add($name, ChoiceType::class, array_merge(self::getOptions([
'choices' => $choices,
'required' => !$this->reflectionUtils->isOrmFieldNullable($builder->getDataClass(), $name),
]), $extraOptions));
}
public function addSelectFormField($name, $options)
{
$addFormCallback = function (FormInterface $form, $item) use ($name, $options) {
$form->add($name, EntityType::class, array_merge([
'class' => null,
'choices' => [],
'choice_label' => function($item) {
return $item->toString();
},
'required' => false,
'empty_data' => 0,
'mapped' => true,
'multiple' => false,
'expanded' => false, // Render as checkboxes
], $options));
};
$this->addFormFieldCreateCallback($addFormCallback);
}
public function addImageField(FormBuilderInterface $builder, string $name, array $options = null,
$acceptImages = true, $acceptVideos = false)
{
$accept = "";
$mimeTypes = [];
if ($acceptImages) {
$accept .= ($accept ? "," : "") . "image/*";
$mimeTypes = array_merge($mimeTypes, [
'image/jpg',
'image/jpeg',
'image/png',
'image/gif',
]);
}
if ($acceptVideos) {
$accept .= ($accept ? "," : "") . "video/*";
$mimeTypes = array_merge($mimeTypes, [
'video/mp4',
'video/mov',
]);
}
$constraints = [];
if ($acceptImages && !$acceptVideos) {
$constraints[] = new Image([]);
} else {
$constraints[] = new File([
'mimeTypes' => $mimeTypes,
'mimeTypesMessage' => 'Недопустимый формат файла',
]);
}
$builder->add($name, FileType::class, array_merge([
'required' => false,
'data' => null,
'attr' => [
'accept' => $accept,
],
'constraints' => $constraints
], $options ?? []));
$builder->add($name . "Remove", CheckboxType::class, [
'mapped' => false,
'required' => false,
]);
}
public function addTextField(string $name, FormBuilderInterface $builder, array $options = null): self
{
$builder->add($name, TextType::class, $options ?? []);
return $this;
}
/**
* @param string|array $name
* @param FormBuilderInterface $builder
* @return void
*/
public function addCheckboxField($name, FormBuilderInterface $builder, array $options = null): self
{
$names = $name;
if (!is_array($names)) {
$names = [$names];
}
$options = array_merge([
'required' => false,
], $options ?? []);
foreach ($names as $name) {
$builder->add($name, CheckboxType::class, $options);
}
return $this;
}
public function addFileField(FormBuilderInterface $builder, string $name, array $fileExtensions = null, array $options = [])
{
$builder->add($name, FileType::class, array_merge(
[
'required' => false,
'data' => null,
'constraints' => [
new File(),
]
],
($fileExtensions ? [
'attr' => [
'accept' => implode(",", $fileExtensions)
]
] : []),
$options
));
}
public function addDateField($fieldName)
{
$this->getBuilder()->add($fieldName, DateType::class, [
'widget' => 'single_text',
]);
}
public function addDateTimeField($fieldName)
{
$this->getBuilder()->add($fieldName, DateTimeType::class, [
'widget' => 'single_text',
]);
}
public function addDateFieldAsTextInput(FormBuilderInterface $builder, $fieldName, bool $disabled = false)
{
Other::forEach($fieldName, function ($fieldName) use ($builder, $disabled) {
$builder->add($fieldName, TextType::class, [
'required' => true,
'disabled' => $disabled,
]);
$builder->get($fieldName)->addModelTransformer($this->dataTransformerRetriever->getStringToDateTransformer());
});
}
/**
* Field entity service should have method: getChoiceLabel, get[EntityClass]Choices(). EntityClass - editing entity class.
* Example:
* Edit Story#carWash field.
* CarWashService should have methods:
* 1. getStoryChoices(Story $story) - get choices (all possible items) for Story's field - carWash.
* 2. getChoiceLabel(CarWash $carWash) - get choice label (html select option text) of CarWash.
* @param FormBuilderInterface $builder
* @param string $fieldEntityClass
* @param array|null $choicesQueryParams
* @return void
*/
protected function addEntityField(FormBuilderInterface $builder, string $fieldEntityClass, array $options = null,
$fieldName = null, $defaultValue = null)
{
$item = $builder->getData();
$entityClassShortName = ReflectionUtils::getClassShortName(get_class($item));
$fieldEntityClassShortName = ReflectionUtils::getClassShortName($fieldEntityClass);
$fieldName = ($fieldName ?: lcfirst($fieldEntityClassShortName));
$fieldEntityServiceGetter = "get" . $fieldEntityClassShortName . "Service";
$choicesGetter = "get" . $entityClassShortName . "Choices";
$choiceLabelGetter = "getChoiceLabel";
$fieldNameUcfirst = $fieldName ? ucfirst($fieldName) : null;
$fieldGetter = "get" . ($fieldNameUcfirst ?: $fieldEntityClassShortName);
$fieldSetter = "set" . ($fieldNameUcfirst ?: $fieldEntityClassShortName);
/** @var BaseEntityService $entityService */
$entityService = $this->serviceRetriever->getService("App\Service\\$fieldEntityClassShortName\\" . $fieldEntityClassShortName . "Service");
$choices = $entityService->$choicesGetter($item);
$currentFieldVal = $item->$fieldGetter();
$options = $this->getOptions($options);
$options = array_merge([
'class' => $fieldEntityClass,
'choices' => $choices,
'choice_label' => function($entity) use ($entityService, $choiceLabelGetter, $item) {
$params = (new \ReflectionMethod($entityService, $choiceLabelGetter))->getParameters();
if (count($params) === 2) {
return $entityService->$choiceLabelGetter($entity, $item);
}
return $entityService->$choiceLabelGetter($entity);
},
'required' => true,
'data' => $currentFieldVal ?? $defaultValue,
'empty_data' => null,
'placeholder' => $options["placeholder"]
], $options);
$builder->add($fieldName, EntityType::class, $options);
}
private function getOptions(array $options = null): array
{
return array_merge([
'required' => true,
"placeholder" => isset($options['required']) && $options['required'] ? 'Не выбрано' : 'Нет',
], $options ?? []);
}
/**
* @param array|string[] $fields
* @return void
*/
public function setImageFields(array $fields)
{
$this->imageFields = $fields;
}
/**
* @param array|string[] $fields
* @return void
*/
public function setTextAreaFields(array $fields)
{
$this->textAreaFields = $fields;
}
/**
* @param array|string[] $fields
* @return void
*/
public function setTextFields(array $fields)
{
$this->textFields = $fields;
}
private function addTextFields(FormBuilderInterface $builder, array $options)
{
foreach ($this->textFields as $field) {
$builder->add($field, TextType::class, [
'required' => false,
]);
}
}
private function addFormFieldCreateCallback($callback)
{
$this->addFormFieldCallbacks[] = $callback;
}
/**
* Регистрирует PRE_SUBMIT listener, который удаляет из формы все поля,
* не пришедшие в теле запроса — Doctrine не тронет их в БД.
* Usage:
* $form = $this->createForm(UserSettingsType::class, $item, [
* 'submitted_fields_only' => true,
* ]);
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'submitted_fields_only' => false,
]);
$resolver->setAllowedTypes('submitted_fields_only', 'bool');
}
protected function handleSubmittedFieldsOnly(FormBuilderInterface $builder): void
{
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
$submittedData = $event->getData() ?? [];
$form = $event->getForm();
foreach (array_keys(iterator_to_array($form)) as $fieldName) {
if (!array_key_exists($fieldName, $submittedData)) {
$form->remove($fieldName);
}
}
});
}
}