En esta tercera entrega de Creando un Crud con Laminas, vamos a crear un formulario para crear y editar tareas para esto vamos a usar el componente Laminas\Form, para agregarle validación vamos a usar la interfaz InputFilterAwareInterface y finalmente vamos a renderizar nuestro formulario en las vistas correspondientes, usando las clases CSS de bootstrap.
Creando el formulario de nueva tarea
Vamos a empezar creando la funcionalidad de guardar la tarea en la base de datos. En el archivo TaskTable.php bajo TodosApp\src\Model, y agrega el siguiente método al final del archivo:
public function saveTask(Task $task)
{
$now = new \DateTime();
$data = [
'title' => $task->title,
'description' => $task->description,
'creation_date' => $now->format('Y-m-d H:i:s'),
'finish_date' => $task->finishDate,
'finished' => 0
];
$id = (int) $task->id;
if ($id === 0 ) {
$this->tableGateway->insert($data);
return;
}
try {
$this->getTask($id);
} catch (\Exception $e){
throw new RuntimeException(sprintf(
'Cannot update task with identifier %d; does not exist',
$id
));
}
$this->tableGateway->update($data, ['id'=>$id]);
}
El método saveTask() recibe como parámetro un objeto de tipo Task, en el cuerpo de la función declaramos la variable $now
que guarda la fecha y hora actual. Enseguida seteamos el array $data con los siguientes datos:
- title: recibe el título de la tarea especificada en
$task
. - description: recibe la descripción de la tarea especificado por el objeto
$task
. - creation_date: se setea con la variable $now y se le aplica el formato
Y-m-d H:i:s
, de esta forma se establece su valor por default. - finish_date: también lo recibe por el objeto
$task
. - finished: por default lo seteamos en 0, quiere decir que por default está marcada como no finalizada.
A continuación seteamos la variable $id
con el identificador que trae el objeto $task
al cual se le aplica un casting para asegurarnos que sea un dígito el que se pasa.
Luego si la variable $id
es igual a 0 entonces lo insertamos a la base de datos, quiere decir que es un nuevo registro.
Seguimos, y tenemos en un bloque try - catch
, en el try
intentamos obtener el registro existente por medio de $id
, en caso de no encontrar el registro cae en el catch
y se lanza una excepción de tipo RuntimeException
con el mensaje: Cannot update album with identifier %d; does not exist.
Y por último, cuando se obtuvo el registro, se actualiza con los datos que hemos seateado antes con la variable $data
.
El componente Laminas\Form
Laminas Form es un bridge entre el modelo de negocios definido en el modelo, para este caso Task y la capa de la vista. Extensión de formularios, factory-backend: nos permite extenderlo y definir tus formularios internamente, así como reutilizar el formulario en la aplicación. Vamos a crear el siguiente formulario con un campo de texto para el nombre de la tarea y la descripción, los otros campos van a ser seteados al momento de crear la task.
Hay dos objetivos en crear la clase para el formulario:
- Mostrar el formulario al usuario para que proporcione los datos.
- Procesar el envío del formulario y almacenarlo en la base de datos.
Vamos a crear el archivo TaskForm.php en la siguiente ruta TodosApp/src/Form/ y vamos copiar y pegar el siguiente código.
<?php
namespace TodosApp\Form;
use Laminas\Form\Form;
class TaskForm extends Form
{
/** TaskForm constructor. */
public function __construct()
{
parent::__construct('task');
$this->add([
'name' => 'id',
'type' => 'hidden'
]);
$this->add([
'name' => 'title',
'type' => 'text',
'options' => [
'label' => 'Titulo'
]
]);
$this->add([
'name' => 'description',
'type' => 'text',
'options' => [
'label' => 'Descripción'
]
]);
$this->add([
'name' => 'submit',
'type' => 'submit',
'attributes' => [
'value' => 'Go',
'id' => 'submitbutton'
]
]);
}
}
Nuestra clase TaskForm extiende de Laminas\Form\Form. En el constructor de la clase seteamos el nombre del formulario mediante el constructor padre. A continuación creamos cuatro elementos: id, title, descripción y un botón de envío. Para cada uno de estos elementos seteamos el nombre del elemento, varios atributos y opciones, incluyendo la etiqueta (label) a ser mostrada.
Para agregar la validación a nuestro formulario, vamos a modificar el archivo Task.php ubicado en TodosApp/src/Model, y agregamos el siguiente código:
public function setInputFilter(InputFilterInterface $inputFilter)
{
throw new \DomainException(sprintf(
'%s does not allow injection of an alternate input filter', __CLASS__
));
}
public function getInputFilter()
{
if ($this->inputFilter) {
return $this->inputFilter;
}
$inputFilter = new InputFilter();
$inputFilter->add([
'name' => 'id',
'required' => true,
'filters' => [
['name' => ToInt::class]
]
]);
$inputFilter->add([
'name' => 'title',
'required' => true,
'filters' => [
['name' => StripTags::class],
['name' => StringTrim::class],
],
'validators' => [
[
'name' => StringLength::class,
'options' => [
'encoding' => 'UTF-8',
'min' => 1,
'max' => 145
],
],
],
]);
$inputFilter->add([
'name' => 'description',
'required' => false,
'filters' => [
['name' => StripTags::class],
['name' => StringTrim::class],
],
'validators' => [
[
'name' => StringLength::class,
'options' => [
'encoding' => 'UTF-8',
'min' => 5,
'max' => 500
],
],
],
]);
$this->inputFilter = $inputFilter;
return $this->inputFilter;
}
También agregamos al inicio del archivo, después de la declaración del namespace, lo siguiente:
use Laminas\Filter\StringTrim;
use Laminas\Filter\StripTags;
use Laminas\Filter\ToInt;
use Laminas\InputFilter\InputFilter;
use Laminas\InputFilter\InputFilterAwareInterface;
use Laminas\InputFilter\InputFilterInterface;
use Laminas\Validator\StringLength;
Y hacemos que nuestra clase de Task sea una implementación de InputFilterAwareInterface, de la siguiente manera:
class Task implements InputFilterAwareInterface
La interfaz InputFilterAwareInterface, utilizará para vincular un filtro de entrada a un formulario a nuestro formulario recién creado. Para usar la validación de InputFilterAwareInterface necesitamos definir dos métodos setInputFilter()
and getInputFilter()
. Nosotros solo necesitamos implementar getInputFilter() por lo que lanzamos una excepción a la implementación setInputFilter(); por si llegamos a usar ese método.
Dentro de getInputFilter(), creamos una instancia de InputFilter. Luego agregamos la validación para id, del cual necesitamos que sea un entero. Para elementos de texto, agregamos los filtros StripTags and StringTrim, para remover HTML no deseado y espacios en blanco innecesarios. También usamos el validador StringLength, para asegurarnos que el usuario no ingrese más caracteres de los necesarios.
A continuación vamos mostrar el formulario en nuestra aplicación para esto vamos a crear la acción CreateAction, dentro de nuestro controlador. Abrimos el controlador ubicado en TodosApp/src/Controller/TodoController.php y agregamos el siguiente código:
public function createAction()
{
$form = new TaskForm();
$form->get('submit')->setValue('Nueva');
$request = $this->getRequest();
if (! $request->isPost()) {
return ['form' => $form];
}
$task = new Task();
$form->setInputFilter($task->getInputFilter());
$form->setData($request->getPost());
if (! $form->isValid()) {
return ['form' => $form];
}
$task->exchangeArray($form->getData());
$this->table->saveTask($task);
return $this->redirect()->toRoute('todos-app-index');
}
E importamos la clase TaskForm al inicio de la clase:
use TodosApp\Form\TaskForm;
En el action createAction(), instanciamos la clase TaskForm() y cambiamos la etiqueta del botón submit a Nueva, hacemos esto por si queremos reusar el mismo formulario para editar una tarea. A continuación se verifica si los datos recibidos no son mediante POST, entonces retornamos el formulario que se va a mostrar, laminas-mvc permite regresar un array en lugar de un objeto view model. Si el formulario fue correctamente enviado por POST, entonces validamos los campos de entrada, el método setInputFilters() establece las reglas de validación de los datos al formulario, a continuación pasamos los datos que se han enviado a la instancia del formulario. Si la validación falla regresamos nuevamente el formulario para volverlo a desplegar. En este punto el formulario contiene qué campos han fallado la validación y por qué, y esta información es comunicada a la capa de la vista. Si el formulario es válido, entonces tomamos los datos del formulario y los guardamos en base de datos mediante el método saveTask(), por último redireccionamos a la lista de tareas.
Agregando el formulario a la vista
A continuación creamos un nuevo archivo llamado create.phtml bajo el directorio TodosApp\src\View\todos-app\to-do y agregamos el siguiente contenido:
<?php
$title = 'Nueva tarea';
$this->headTitle($title);
?>
<h4 class="d-flex justify-content-between align-items-center mb-3 mt-3">
<span class="text-muted"><?=$title?></span>
</h4>
<?php
$form->setAttribute('action', $this->url('todo-app', ['action' => 'create']));
$form->prepare();
echo $this->form()->openTag($form);
echo $this->formHidden($form->get('id'));
echo $this->formRow($form->get('title'));
echo $this->formRow($form->get('description'));
echo $this->formSubmit($form->get('submit'));
echo $this->form()->closeTag();
En esta vista estamos desplegando el formulario, primero seteamos un título para la vista en este caso Nueva Tarea, después con $form->setAttribute();
seteamos la ruta del formulario a donde vamos a enviar los datos para ser guardados.
El helper form() tiene un método openTag() y closeTag() para abrir y cerrar el formulario. Para cada elemento con un label podemos usar formRow() para renderizar la etiqueta y cualquier mensaje de error de validación. Por último para los elementos que son independientes y no tienen reglas de validación usamos formHidden() y formSubmit().
Si abrimos en el navegador el listado de tareas y damos click en el botón Crear, nos va a llevar a nuestro formulario, el cual no luce muy bonito.
Esto es debido a que no estamos usando las clases que nos proporciona boostrap 4, la base CSS utilizada en el diseño del layout. Vamos a modificar la vista para que el formulario sea más agradable a la vista, esto renderizando etiquetas, elementos y mensajes de error separadamente y agregando atributos a los elementos. Actualizamos para mostrar como sigue:
<?php
$title = $form->get('title');
$title->setAttribute('class', 'form-control');
$title->setAttribute('placeholder', 'Título');
$description = $form->get('description');
$description->setAttribute('class', 'form-control');
$description->setAttribute('placeholder', 'Descripción');
$submit = $form->get('submit');
$submit->setAttribute('class', 'btn btn-primary');
$form->setAttribute('action', $this->url('todo-app', ['action' => 'create']));
$form->prepare();
echo $this->form()->openTag($form);
?>
<div class="form-group">
<?= $this->formLabel($title) ?>
<?= $this->formElement($title) ?>
<?= $this->formElementErrors()->render($title, ['class' => 'help-block text-danger']) ?>
</div>
<div class="form-group">
<?= $this->formLabel($description) ?>
<?= $this->formElement($description) ?>
<?= $this->formElementErrors()->render($description, ['class' => 'help-block text-danger']) ?>
</div>
<?php
echo $this->formSubmit($submit);
echo $this->formHidden($form->get('id'));
echo $this->form()->closeTag();
?>
Ahora si actualizamos el navegador podemos ver esto:
Que ya luce mucho mejor.
Editar tarea
La acción para editar una tarea es muy similar a la acción de crear. En nuestro controllador ToDoController vamos a agregar lo siguiente:
public function editAction()
{
$id = (int) $this->params()->fromRoute('id', 0);
if (0 === $id) {
return $this->redirect()->toRoute('todo-app-create', ['action' => 'create']);
}
try {
$task = $this->table->getTask($id);
} catch (\Exception $e) {
return $this->redirect()->toRoute('todo-app', ['action' => 'index']);
}
$form = new TaskForm();
$form->bind($task);
$form->get('submit')->setAttribute('value', 'Editar');
$request = $this->getRequest();
$viewData = ['id' => $id, 'form' => $form];
if (!$request->isPost()) {
return $viewData;
}
$form->setInputFilter($task->getInputFilter());
$form->setData($request->getPost());
if (!$form->isValid()) {
return $viewData;
}
try {
$this->table->saveTask($task);
} catch (Exception $e){
\error_log("error updating", $e->getMessage());
}
return $this->redirect()->toRoute('todo-app', ['action' => 'index']);
}
Lo primero que hacemos es capturar el id que es pasado mediante la ruta y aplicarle un casting, que básicamente significa convertir a entero el contenido del parámetro id. Posteriormente si la conversión es igual a 0 entonces redirigimos la página al listado de tasks. De lo contrario si el id es diferente de 0 entonces cargamos la task seleccionada mediante el método getTask(). Si por alguna razón no se encuentra la task el método de acceso a datos arroja una excepción. Detectamos esa excepción y redirigimos al usuario a la página del listado.
A continuación creamos un objeto de tipo TaskForm, le adjuntamos los datos del modelo (los datos de la tarea que buscamos) al formulario y en el atributo submit le cambiamos la etiqueta al botón para que despliegue la palabra Editar. El método bind() hace dos cosas:
- Despliega el formulario con los valores iniciales del modelo.
- Después de la validación isValid(), coloca los valores que se modifican en el formulario en el modelo.
Las operaciones anteriores son hechas por el objeto hydrator.
La hydratación es el acto de poblar un objeto desde un conjunto de datos, laminas-hydrator es un component que proporciona mecanismos para hidratar objetos, así como también para extraer datos. Laminas\Hydrator\ArraySerializable espera dos métodos en el modelo: getArrayCopy() y exchangeArray(); ya hemos escrito este último en la entidad Task, así que ahora necesitamos definir el método getArrayCopy(), agregamos lo siguiente:
public function getArrayCopy(): array
{
return [
'id' => $this->id,
'title' => $this->title,
'description' => $this->description,
];
}
Con este último método, a través de la hidratación, cuando validamos, logramos que los datos del formulario se copien al modelo y con saveTask() guardamos la información en la base de datos.
Agregando el formulario de edición a la vista
A continuación creamos un nuevo archivo llamado edit.phtml bajo el directorio TodosApp\src\View\todos-app\to-do y agregamos el siguiente contenido:
<?php
$title = 'Editar tarea';
$this->headTitle($title);
?>
<h4 class="d-flex justify-content-between align-items-center mb-3 mt-3">
<span class="text-muted"><?=$title?></span>
</h4>
<?php
$task = $form->get('title');
$task->setAttribute('class', 'form-control');
$task->setAttribute('placeholder', 'Titulo');
$description = $form->get('description');
$description->setAttribute('class', 'form-control');
$description->setAttribute('placeholder', 'Descripción');
$submit = $form->get('submit');
$submit->setAttribute('class', 'btn btn-primary');
$form->setAttribute('action', $this->url('todo-app', ['action' => 'edit', 'id' => $id]));
$form->prepare();
echo $this->form()->openTag($form);
?>
<div class="form-group">
<?= $this->formLabel($task) ?>
<?= $this->formElement($task) ?>
<?= $this->formElementErrors()->render($task, ['class' => 'help-block text-danger']) ?>
</div>
<div class="form-group">
<?= $this->formLabel($description) ?>
<?= $this->formElement($description) ?>
<?= $this->formElementErrors()->render($description, ['class' => 'help-block text-danger']) ?>
</div>
<?php
echo $this->formSubmit($submit);
echo $this->formHidden($form->get('id'));
echo $this->form()->closeTag();
?>
Como nos podemos dar cuenta el formulario de editar es muy similar al formulario de creación de una nueva task, con la diferencia que se le setea el título de la página a la url de edición del formulario y viene con los datos del id seleccionado. La siguiente imagen muestra cómo deberías ver el formulario:
Una vez que editas la tarea al darle click en editar, se va a modificar la tarea con la información nueva que hayas proporcionado y a continuación te va a redirigir al listado.
Es todo por ahora, en la cuarta parte (espero que sea la última) vamos a implementar la visualización de una tarea con opción de marcarla como completada y vamos a eliminar una tarea y con eso terminamos.
Saludos y happy coding :)