Cómo trabajar con el tipo de datos de Point en Doctrine 2 y Symfony 5

Si estás almacenando coordenadas de una ubicación en tu base de datos MySQL, es posible que sepas que es muy conveniente almacenarlas en columnas de tipo Point en tus tablas. Esto generará consultas futuras bastante eficientes que es posible que debas ejecutar al buscar ubicaciones más cercanas, etc. De forma predeterminada, si utilizas Symfony CRUD para crear formularios y el usuario intenta enviarlo, habrá un error al intentar convertir las coordenadas de una cadena de texto a un Point en la base de datos.

En este artículo, te explicaré cómo almacenar y manipular fácilmente campos de tipo Point en tu base de datos usando Doctrine ORM en Symfony 5.

1. Instala la extensión espacial de Doctrine 2

Para trabajar con el tipo de datos Point en Doctrine, necesitas la Extensión espacial de Doctrine . Esta biblioteca de Doctrine2 ofrece soporte multiplataforma para tipos y funciones espaciales. Actualmente, se admiten MySQL y PostgreSQL con PostGIS. Los siguientes tipos de SQL / OpenGIS se han implementado como objetos PHP y los tipos de Doctrine que los acompañan:

  • Geometry
  • Point
  • LineString
  • Polygon
  • MultiPoint
  • MultiLineString
  • MultiPolygon
  • Geography

Similar a Geometry, pero siempre se usa el valor SRID (SRID solo es compatible con PostGIS) y solo acepta coordenadas "geográficas" válidas.

  • Point
  • LineString
  • Polygon

Hay soporte para valores de retorno WKB / WKT y EWKB / EWKT. Actualmente, solo se utiliza WKT / EWKT en las declaraciones. Para instalar esta extensión, ejecute el siguiente comando en tu proyecto Symfony:

composer require creof/doctrine2-spatial

Para obtener más información sobre esta biblioteca, visite el repositorio oficial en Github aquí . Después de instalar la extensión, asegúrese de habilitar el tipo de punto en tu archivo de configuración doctrine.yaml:

# project/config/packages/doctrine.yaml
doctrine:
    dbal:
        url: '%env(resolve:DATABASE_URL)%'
        types:
            point: CrEOF\Spatial\DBAL\Types\Geometry\PointType

2. Creando Entidad

Ya sea si estás creando la entidad manualmente o decidiste usar el comando doctrine:mapping:import de una base de datos existente, la entidad se verá así asumiendo que tiene al menos 1 campo de tipo punto en tu base de datos, en nuestro caso, el nombre de la columna es coordinates:

<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

use CrEOF\Spatial\PHP\Types\Geometry\Point;

/**
 * ExampleEntity
 *
 * @ORM\Table(name="example", indexes={@ORM\Index(name="coordinates", columns={"coordinates"})})
 * @ORM\Entity
 */
class ExampleEntity
{
    /**
     * @var point
     *
     * @ORM\Column(name="coordinates", type="point", nullable=true)
     */
    private $coordinates;

    /**
     * Get the coordinates property.
     * 
     * @return Point
     */
    public function getCoordinates()
    {
        return $this->coordinates;
    }
    
    /**
     * Set the coordinates property.
     * 
     * @param Point $coordinates
     * @return self
     */
    public function setCoordinates(Point $coordinates): self
    {
        $this->coordinates = $coordinates;

        return $this;
    }
}

Tenga en cuenta que debe esperar una clase de tipo Point en el método setter (de la extensión espacial) y el getter devolverá automáticamente un objeto de tipo Point.

3. Datos persistentes

Decidí crear el crud automáticamente usando la utilidad make de Symfony:

php bin/console make:crud

Obtendrás asi automáticamente la ruta, el tipo de formulario y el controlador para manipular los datos de la tabla en tu aplicación. Así que si accedes a la ruta, normalmente todo funcionará como de costumbre:

CRUD Symfony

Como puedes ver, el campo de coordenadas es por defecto un campo de texto. En este campo deberías poder insertar las coordenadas proporcionando una cadena con la longitud y latitud (X e Y) respectivamente, separándola con un espacio en blanco, por ejemplo -76.07 4.66. El problema en este momento es que si intenta almacenar una entidad sin modificar la lógica predeterminada, siempre obtendrás la siguiente excepción (los valores de columna de geometría deben implementar GeometryInterface):

Geometry Column Interface

Esto sucede porque básicamente estás persistiendo texto sin formato ( -76.07 4.66) en un campo que espera un objeto Geometry (en este caso, el tipo de datos de puntos). Para resolver esto, debe asegurarse de que el tipo de datos correcto se conserve en la base de datos. Hay varias formas de hacer esto, la más fácil es simplemente preprocesar los datos del campo de coordenadas, creando un CallbackTransformer en el FormType de tu entidad de esta manera:

<?php

namespace App\Form;

use App\Entity\Example;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

// Importar clases requeridas
use Symfony\Component\Form\CallbackTransformer;
use CrEOF\Spatial\PHP\Types\Geometry\Point;

class ExampleType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        // 1. Crear campos de formulario
        $builder
            ->add('name')
            ->add('coordinates')
        ;
        
        // 2. Cree el transformador de modelo para el campo de coordenadas, convirtiendo
        // el punto a una cadena de texto y viceversa
        $builder->get("coordinates")->addModelTransformer(new CallbackTransformer(
            // Transforma el punto en una cadena de texto
            function (?Point $point) {
                if(is_null($point)) return "0 0";
                
                // e.g "-74.07867091 4.66455174"
                return "{$point->getX()} {$point->getY()}";
            },
            // Transforma la cadena de texto de nuevo a un tipo de punto
            function (string $coordinates) {
                // e,g "-74.07867091 4.66455174"
                // lng x $coordinates[0] -74.07867091
                // lat y $coordinates[1] 4.66455174
                $coordinates = explode(" ", $coordinates);
                
                return new Point($coordinates[0], $coordinates[1], null);
            }
        ));
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => Example::class,
        ]);
    }
}

El transformador de devolución de llamada convertirá las coordenadas como una cadena en un objeto Point y viceversa. En tus vistas, el campo de coordenadas será también un objeto Punto, donde puedes obtener cada coordenada por separado (cuando se imprimen las coordenadas como una cadena, contendrá ambas coordenadas):

{# Imprimir longitud #}
{{ entity.coordinates.x }}
{# Imprimir latitud #}
{{ entity.coordinates.y }}

Si alguna vez necesitas almacenar una entidad que contiene un campo de tipo Punto manualmente desde un controlador o servicio, puede conservarlo y proporcionar una instancia de Punto como el valor del campo:

<?php

$entity = new YourEntity();

$xOrLng = -74.07867091;
$yOrLat = 4.66455174;

$point = new \CrEOF\Spatial\PHP\Types\Geometry\Point($xOrLng, $yOrLat, null);

$entity->setCoordinates($point);

$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($entity);
$entityManager->flush();

Que te diviertas ❤️!

Esto podria interesarte

Conviertete en un programador más sociable