Commit 2625e1c5 authored by Laurent Lécluse's avatar Laurent Lécluse
Browse files

Mise en place du générateur de formulaire à partir de définitions d'entités

travail à poursuivre sur StatutSaisieForm
parent 4892d2ac
......@@ -14,7 +14,6 @@ class MotifNonPaiementController extends AbstractController
use MotifNonPaiementSaisieFormAwareTrait;
public function indexAction()
{
$this->em()->getFilters()->enable('historique')->init([
......@@ -33,7 +32,7 @@ class MotifNonPaiementController extends AbstractController
/* @var $motifNonPaiement MotifNonPaiement */
$motifNonPaiement = $this->getEvent()->getParam('motifNonPaiement');
$form = $this->getFormMotifNonPaiementSaisie();
$form = $this->getFormMotifNonPaiementMotifNonPaiementSaisie();
if (empty($motifNonPaiement)) {
$title = 'Création d\'un nouveau motif de non paiement';
$motifNonPaiement = $this->getServiceMotifNonPaiement()->newEntity();
......@@ -41,13 +40,12 @@ class MotifNonPaiementController extends AbstractController
$title = 'Édition d\'un motif de non paiement';
}
$form->bindRequestSave($motifNonPaiement, $this->getRequest(), function (MotifNonPaiement $fr) {
$form->bindRequestSave($motifNonPaiement, $this->getRequest(), function (MotifNonPaiement $motifNonPaiement) {
try {
$this->getServiceMotifNonPaiement()->save($fr);
$this->getServiceMotifNonPaiement()->save($motifNonPaiement);
$this->flashMessenger()->addSuccessMessage('Enregistrement effectué');
} catch (\Exception $e) {
$e = DbException::translate($e);
$this->flashMessenger()->addErrorMessage($e->getMessage() . ':' . $fr->getId());
$this->flashMessenger()->addErrorMessage($this->translate($e));
}
});
......@@ -62,11 +60,11 @@ class MotifNonPaiementController extends AbstractController
try {
$this->getServiceMotifNonPaiement()->delete($motifNonPaiement);
$this->flashMessenger()->addSuccessMessage("Motif de non paiement supprimée avec succès.");
$this->flashMessenger()->addSuccessMessage("Motif de non paiement supprimé avec succès.");
} catch (\Exception $e) {
$this->flashMessenger()->addErrorMessage(DbException::translate($e)->getMessage());
$this->flashMessenger()->addErrorMessage($this->translate($e));
}
return new MessengerViewModel(compact('motifNonPaiement'));
return new MessengerViewModel();
}
}
......@@ -2,147 +2,79 @@
namespace Application\Entity\Db;
use Doctrine\ORM\Mapping as ORM;
use UnicaenApp\Entity\HistoriqueAwareInterface;
use UnicaenApp\Entity\HistoriqueAwareTrait;
/**
* MotifNonPaiement
*/
class MotifNonPaiement implements HistoriqueAwareInterface
{
use HistoriqueAwareTrait;
/**
* @var string
*/
protected $code;
protected ?int $id = null;
/**
* @var string
*/
protected $libelleCourt;
protected ?string $code = null;
/**
* @var string
*/
protected $libelleLong;
protected ?string $libelleCourt = null;
/**
* @var integer
*/
protected $id;
protected ?string $libelleLong = null;
/**
* Set code
*
* @param string $code
*
* @return MotifNonPaiement
*/
public function setCode($code)
public function getId(): ?int
{
$this->code = $code;
return $this;
return $this->id;
}
/**
* Get code
*
* @return string
*/
public function getCode()
public function getCode(): ?string
{
return $this->code;
}
/**
* Set libelleCourt
*
* @param string $libelleCourt
*
* @return MotifNonPaiement
*/
public function setLibelleCourt($libelleCourt)
public function setCode(?string $code): MotifNonPaiement
{
$this->libelleCourt = $libelleCourt;
$this->code = $code;
return $this;
}
/**
* Get libelleCourt
*
* @return string
*/
public function getLibelleCourt()
public function getLibelleCourt(): ?string
{
return $this->libelleCourt;
}
/**
* Set libelleLong
*
* @param string $libelleLong
*
* @return MotifNonPaiement
*/
public function setLibelleLong($libelleLong)
public function setLibelleCourt(?string $libelleCourt): MotifNonPaiement
{
$this->libelleLong = $libelleLong;
$this->libelleCourt = $libelleCourt;
return $this;
}
/**
* Get libelleLong
*
* @return string
*/
public function getLibelleLong()
public function getLibelleLong(): ?string
{
return $this->libelleLong;
}
/**
* Get id
*
* @return integer
*/
public function getId()
public function setLibelleLong(?string $libelleLong): MotifNonPaiement
{
return $this->id;
}
$this->libelleLong = $libelleLong;
return $this;
}
/**************************************************************************************************
* Début ajout
**************************************************************************************************/
/**
* Retourne la représentation littérale de cet objet.
*
* @return string
*/
public function __toString()
public function __toString(): string
{
return $this->getLibelleLong() ?: $this->getLibelleCourt();
}
......
......@@ -3,28 +3,56 @@
namespace Application\Form;
use Application\Constants;
use Application\Filter\FloatFromString;
use Application\Hydrator\GenericHydrator;
use Application\Interfaces\ParametreEntityInterface;
use Application\Service\AbstractEntityService;
use Application\Traits\TranslatorTrait;
use Doctrine\ORM\EntityManager;
use Laminas\Form\Element\Checkbox;
use Laminas\Form\Element\Csrf;
use Laminas\Form\Element\Number;
use Laminas\Form\Element\Select;
use Laminas\Form\Element\Text;
use Laminas\Form\Form;
use Laminas\Http\Request;
use Laminas\InputFilter\InputFilterProviderInterface;
use Laminas\Mvc\Controller\Plugin\FlashMessenger;
use Laminas\Stdlib\ArrayUtils;
use UnicaenApp\Entity\HistoriqueAwareInterface;
abstract class AbstractForm extends Form implements InputFilterProviderInterface
{
use TranslatorTrait;
/**
* @var FlashMessenger
*/
private $controllerPluginFlashMessenger;
private ?FlashMessenger $controllerPluginFlashMessenger = null;
/**
* @var \Exception
*/
private $exception;
private ?EntityManager $entityManager = null;
private array $spec = [];
protected function getEntityManager(): EntityManager
{
if (!$this->entityManager) {
$this->entityManager = \Application::$container->get(Constants::BDD);
}
return $this->entityManager;
}
private function getControllerPluginFlashMessenger(): FlashMessenger
{
if (!$this->controllerPluginFlashMessenger) {
$this->controllerPluginFlashMessenger = \Application::$container->get('ControllerPluginManager')->get('flashMessenger');
}
return $this->controllerPluginFlashMessenger;
}
......@@ -68,12 +96,10 @@ abstract class AbstractForm extends Form implements InputFilterProviderInterface
*/
protected function useGenericHydrator(array $hydratorElements, ?string $hydratorClass = null): GenericHydrator
{
$em = \Application::$container->get(Constants::BDD);
if ($hydratorClass) {
$hydrator = new $hydratorClass($em, $hydratorElements);
$hydrator = new $hydratorClass($this->getEntityManager(), $hydratorElements);
} else {
$hydrator = new GenericHydrator($em, $hydratorElements);
$hydrator = new GenericHydrator($this->getEntityManager(), $hydratorElements);
}
$this->setHydrator($hydrator);
......@@ -83,6 +109,158 @@ abstract class AbstractForm extends Form implements InputFilterProviderInterface
public function spec(string|object|array $spec, array $ignore = [])
{
if (is_string($spec) && class_exists($spec)) {
return $this->specFromClass($spec, $ignore);
}
if (is_object($spec)) {
return $this->specFromObject($spec, $ignore);
}
if (is_array($spec)) {
return $this->specFromArray($spec, $ignore);
}
throw new \Exception('La spécification fournie n\'est pas exploitable');
}
public function specElement(string $elementName, array $elSpec)
{
if (!isset($this->spec[$elementName])) {
$this->spec[$elementName] = [];
}
if (!isset($this->spec[$elementName]['element'])) {
$this->spec[$elementName]['element'] = [];
}
$this->spec[$elementName]['element'] = ArrayUtils::merge($this->spec[$elementName]['element'], $elSpec);
}
public function specBuild()
{
$this->useGenericHydrator($this->spec);
foreach ($this->spec as $elName => $elSpec) {
if (isset($elSpec['element'])) {
$this->add($elSpec['element']);
}
}
$this->add(new Csrf('security'));
}
private function specFromClass(string $class, array $ignore): self
{
$elements = [];
$rc = new \ReflectionClass($class);
$methods = $rc->getMethods();
if ($rc->implementsInterface(HistoriqueAwareInterface::class)) {
$ignore[] = 'histoCreation';
$ignore[] = 'histoCreateur';
$ignore[] = 'histoModification';
$ignore[] = 'histoModificateur';
$ignore[] = 'histoDestruction';
$ignore[] = 'histoDestructeur';
}
if ($rc->implementsInterface(ParametreEntityInterface::class)) {
$ignore[] = 'annee';
}
foreach ($methods as $method) {
$property = null;
if (str_starts_with($method->name, 'get')) {
$property = substr($method->name, 3);
} elseif (str_starts_with($method->name, 'is')) {
$property = substr($method->name, 2);
}
if ($property) {
if (!$rc->hasMethod('set' . $property)) {
$property = null;
}
}
if ($property) {
$elKey = lcfirst($property);
if (!in_array($elKey, $ignore)) {
$element = [
'getter' => $method->name,
'setter' => 'set' . $property,
];
if ($method->hasReturnType()) {
$rt = $method->getReturnType();
if ($rt instanceof \ReflectionNamedType) {
$element['type'] = $rt->getName();
} elseif ($rt instanceof \ReflectionUnionType) {
$element['type'] = $rt->getTypes()[0]->getName();
}
}
$elements[$elKey] = $element;
}
}
}
/* Si c'est une entité Doctrine, on récupère les infos du mapping */
try {
$cmd = $this->getEntityManager()->getClassMetadata($class);
} catch (\Exception $e) {
$cmd = null;
}
if (!empty($elements) && !empty($cmd)) {
foreach ($elements as $property => $element) {
if ($cmd->hasField($property)) {
$mapping = $cmd->getFieldMapping($property);
$this->elementAddMapping($elements[$property], $mapping);
}
}
}
/* Ajout d'un élément caché pour l'ID */
if ($cmd && $cmd->hasField('id')) {
$this->spec(['id' => ['element' => ['type' => 'Hidden', 'name' => 'id']]]);
}
/* Construction des éléments de formulaires */
if (!empty($elements)) {
foreach ($elements as $property => $element) {
$this->makeElement($property, $elements[$property]);
}
}
$this->specFromArray($elements, []);
return $this;
}
private function specFromObject(object $object, array $ignore): self
{
return $this->specFromClass(get_class($object), $ignore);
}
private function specFromArray(array $spec, array $ignore): self
{
foreach ($spec as $k => $v) {
if (in_array($k, $ignore)) {
unset($spec[$k]);
}
}
$this->spec = ArrayUtils::merge($this->spec, $spec);
return $this;
}
/**
* Exécute la sauvegarde d'un formulaire à partir des données Request
*
......@@ -98,12 +276,11 @@ abstract class AbstractForm extends Form implements InputFilterProviderInterface
*
* @return bool
*/
public function bindRequestSave($entity, Request $request, $saveFnc, $successMessage = 'Enregistrement effectué')
public function bindRequestSave($entity, Request $request, $saveFnc, string $successMessage = 'Enregistrement effectué'): bool
{
$this->exception = null;
$this->bind($entity);
if ($request->isPost()) {
$data = array_merge_recursive(
$data = ArrayUtils::merge(
$request->getPost()->toArray(),
$request->getFiles()->toArray()
);
......@@ -121,7 +298,6 @@ abstract class AbstractForm extends Form implements InputFilterProviderInterface
try {
$saveFnc($entity);
} catch (\Exception $e) {
$this->exception = $e;
$this->getControllerPluginFlashMessenger()->addErrorMessage($e->getMessage());
return false;
......@@ -135,29 +311,6 @@ abstract class AbstractForm extends Form implements InputFilterProviderInterface
/**
* @param $entity
* @param AbstractEntityService $service
* @param string $successMessage
*
* @return bool
*/
public function delete($entity, AbstractEntityService $service, $successMessage = 'Donnée supprimée avec succès.')
{
try {
$service->delete($entity);
$this->getControllerPluginFlashMessenger()->addSuccessMessage($successMessage);
} catch (\Exception $e) {
$this->getControllerPluginFlashMessenger()->addErrorMessage($this->translate($e->getMessage()));
return false;
}
return true;
}
/**
* Exécute la sauvegarde d'un formulaire à partir des données Request
*
......@@ -171,16 +324,14 @@ abstract class AbstractForm extends Form implements InputFilterProviderInterface
*
* @return bool
*/
public function requestSave(Request $request, $saveFnc)
public function requestSave(Request $request, $saveFnc): bool
{
$this->exception = null;
if ($request->isPost()) {
$this->setData($request->getPost());
if ($this->isValid()) {
try {
$saveFnc($this->getData());
} catch (\Exception $e) {
$this->exception = $e;
$this->getControllerPluginFlashMessenger()->addErrorMessage($e->getMessage());
return false;
......@@ -193,25 +344,172 @@ abstract class AbstractForm extends Form implements InputFilterProviderInterface
/**
* @return \Exception
*/
public function getException()
public function readOnly(bool $readOnly)
{
return $this->exception;
/** @var $element \Laminas\Form\Element */
foreach ($this->getElements() as $element) {
switch (get_class($element)) {
case Number::class:
case Text::class:
$element->setAttribute('readonly', $readOnly);
break;
case Select::class:
case Checkbox::class:
$element->setAttribute('disabled', $readOnly);
break;
}
}
}
/**
* @return FlashMessenger
*/
private function getControllerPluginFlashMessenger()
public function getInputFilterSpecification()
{
if (!$this->controllerPluginFlashMessenger) {
$this->controllerPluginFlashMessenger = \Application::$container->get('ControllerPluginManager')->get('flashMessenger');
$filters = [];
foreach ($this->spec as $name => $spec) {
if (isset($spec['filter'])) {
$filters[$name] = $spec['filter'];
}
}
return $this->controllerPluginFlashMessenger;
return $filters;
}
private function elementAddMapping(array &$element, array $mapping)
{
/* Gestion du Required */
if (isset($mapping['nullable'])) {
if (!isset($element['controls'])) {
$element['controls'] = [];
}
$element['controls']['required'] = !$mapping['nullable'];
}
/* Gestion des length */
if (($mapping['type'] ?? '') == 'string' && isset($mapping['length']) && $mapping['length']) {
$validator = [
'name' => 'StringLength',
'options' => ['max' => $mapping['length']],
];
$this->elementAddValidator($element, $validator);
}
}
protected function elementAddValidator(array &$element, array $validatorConfig)
{
if (!isset($element['controls'])) {
$element['controls'] = [];
}
if (!isset($element['controls']['validators'])) {
$element['controls']['validators'] = [];
}
$element['controls']['validators'][] = $validatorConfig;
}