EasyAdminBundle NumberField is rounding latitude and longitude decimals

1k views Asked by At

I am using EasyAdminBundle in a PHP Symfony project. In the admin, I have a problem displaying the NumberFields latitude and longitude in the Edit and Detail view, because EasyAdmin does not display the values stored in the database, these values are rounded to only 3 decimals instead of all the decimals stored in the database.

You can see my ActionCrudController below:

<?php

namespace App\Controller\Admin;

use App\Entity\Action;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
use Symfony\Component\Routing\Annotation\Route;
use EasyCorp\Bundle\EasyAdminBundle\Field\Field;
use EasyCorp\Bundle\EasyAdminBundle\Field\EmailField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
use EasyCorp\Bundle\EasyAdminBundle\Field\IdField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TelephoneField;
use EasyCorp\Bundle\EasyAdminBundle\Field\UrlField;
use EasyCorp\Bundle\EasyAdminBundle\Field\BooleanField;
use EasyCorp\Bundle\EasyAdminBundle\Field\DateTimeField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextareaField;
use EasyCorp\Bundle\EasyAdminBundle\Field\NumberField;
use EasyCorp\Bundle\EasyAdminBundle\Field\AssociationField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextEditorField;

use EasyCorp\Bundle\EasyAdminBundle\Config\Action as EasyCorpAction;
use EasyCorp\Bundle\EasyAdminBundle\Config\Actions;
use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;

class ActionCrudController extends AbstractCrudController
{

    public static function getEntityFqcn(): string
    {
        return Action::class;
    }

    public function configureFields(string $pageName): iterable
    {
        return [
            IdField::new('id')->hideOnForm(),
            TextField::new('name'),
            TextEditorField::new('description')->hideOnIndex(),
            TextareaField::new('address'),
            TextField::new('city')->hideOnIndex(),
            TextField::new('postal_code')->hideOnIndex(),
            TextField::new('email'),
            TelephoneField::new('phone'),
            UrlField::new('website')->hideOnIndex(),
            TextEditorField::new('opening_hours'),
            NumberField::new('latitude')->hideOnIndex(),
            NumberField::new('longitude')->hideOnIndex(),
            TextareaField::new('public_accueilli_detail')->hideOnIndex(),
            TextareaField::new('modalite_acces')->hideOnIndex(),
            TextareaField::new('tarif')->hideOnIndex(),
            TextField::new('slug')->hideOnIndex(),
            DateTimeField::new('created_at')->onlyOnIndex(),
            DateTimeField::new('updated_at')->onlyOnIndex(),
            AssociationField::new('operateur')->setRequired(true),
            AssociationField::new('sousthematiques'),
            AssociationField::new('public_accueilli')->hideOnIndex(),
        ];
    }

    public function configureActions(Actions $actions): Actions
    {
        return $actions
            ->add(Crud::PAGE_INDEX, EasyCorpAction::DETAIL);
    }
}

And you can see my Action Entity here where latitude and longitude are float:

<?php

namespace App\Entity;

use App\Repository\ActionRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass=ActionRepository::class)
 * @ORM\HasLifecycleCallbacks()
 * @ORM\Table("action")
 */
class Action
{
    use Timestamps;

    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="string", length=255)
     */
    private $name;

    /**
     * @ORM\Column(type="text", nullable=true)
     */
    private $description;

    /**
     * @ORM\Column(type="string", length=255, nullable=true)
     */
    private $address;

    /**
     * @ORM\Column(type="string", length=255, nullable=true)
     */
    private $city;

    /**
     * @ORM\Column(type="string", length=255, nullable=true)
     */
    private $postal_code;

    /**
     * @ORM\Column(type="string", length=255, nullable=true)
     */
    private $email;

    /**
     * @ORM\Column(type="string", length=255, nullable=true)
     */
    private $phone;

    /**
     * @ORM\Column(type="string", length=255, nullable=true)
     */
    private $website;

    /**
     * @ORM\Column(type="text", nullable=true)
     */
    private $opening_hours;

    /**
     * @ORM\Column(type="float")
     */
    private $latitude;

    /**
     * @ORM\Column(type="float")
     */
    private $longitude;

    /**
     * @ORM\Column(type="string", length=255, nullable=true)
     */
    private $public_accueilli_detail;

    /**
     * @ORM\Column(type="string", length=255, nullable=true)
     */
    private $modalite_acces;

    /**
     * @ORM\Column(type="string", length=255, nullable=true)
     */
    private $tarif;

    /**
     * @ORM\Column(type="string", length=255, nullable=true)
     */
    private $slug;

    /**
     * @ORM\Column(type="datetime")
     */
    private $created_at;

    /**
     * @ORM\Column(type="datetime", nullable=true)
     */
    private $updated_at;

    /**
     * @ORM\ManyToOne(targetEntity=Operateur::class, inversedBy="actions")
     * @ORM\JoinColumn(nullable=false)
     */
    private $operateur;

    /**
     * @ORM\ManyToMany(targetEntity=SousThematique::class, inversedBy="actions")
     */
    private $sousthematiques;

    /**
     * @ORM\OneToMany(targetEntity=HoraireAction::class, mappedBy="action")
     */
    private $horaire_actions;

    /**
     * @ORM\ManyToMany(targetEntity=PublicAccueilli::class, inversedBy="actions")
     */
    private $public_accueilli;

    public function __construct()
    {
        $this->sousthematiques = new ArrayCollection();
        $this->horaire_actions = new ArrayCollection();
        $this->public_accueilli = new ArrayCollection();
    }

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getName(): ?string
    {
        return $this->name;
    }

    public function setName(string $name): self
    {
        $this->name = $name;

        return $this;
    }

    public function getDescription(): ?string
    {
        return $this->description;
    }

    public function setDescription(?string $description): self
    {
        $this->description = $description;

        return $this;
    }

    public function getAddress(): ?string
    {
        return $this->address;
    }

    public function setAddress(?string $address): self
    {
        $this->address = $address;

        return $this;
    }

    public function getCity(): ?string
    {
        return $this->city;
    }

    public function setCity(?string $city): self
    {
        $this->city = $city;

        return $this;
    }

    public function getPostalCode(): ?string
    {
        return $this->postal_code;
    }

    public function setPostalCode(?string $postal_code): self
    {
        $this->postal_code = $postal_code;

        return $this;
    }

    public function getEmail(): ?string
    {
        return $this->email;
    }

    public function setEmail(?string $email): self
    {
        $this->email = $email;

        return $this;
    }

    public function getPhone(): ?string
    {
        return $this->phone;
    }

    public function setPhone(?string $phone): self
    {
        $this->phone = $phone;

        return $this;
    }

    public function getWebsite(): ?string
    {
        return $this->website;
    }

    public function setWebsite(?string $website): self
    {
        $this->website = $website;

        return $this;
    }

    public function getOpeningHours(): ?string
    {
        return $this->opening_hours;
    }

    public function setOpeningHours(?string $opening_hours): self
    {
        $this->opening_hours = $opening_hours;

        return $this;
    }

    public function getLatitude(): ?float
    {
        return $this->latitude;
    }

    public function setLatitude(float $latitude): self
    {
        $this->latitude = $latitude;

        return $this;
    }

    public function getLongitude(): ?float
    {
        return $this->longitude;
    }

    public function setLongitude(float $longitude): self
    {
        $this->longitude = $longitude;

        return $this;
    }

    public function getPublicAccueilliDetail(): ?string
    {
        return $this->public_accueilli_detail;
    }

    public function setPublicAccueilliDetail(?string $public_accueilli_detail): self
    {
        $this->public_accueilli_detail = $public_accueilli_detail;

        return $this;
    }

    public function getModaliteAcces(): ?string
    {
        return $this->modalite_acces;
    }

    public function setModaliteAcces(?string $modalite_acces): self
    {
        $this->modalite_acces = $modalite_acces;

        return $this;
    }

    public function getTarif(): ?string
    {
        return $this->tarif;
    }

    public function setTarif(?string $tarif): self
    {
        $this->tarif = $tarif;

        return $this;
    }

    public function getSlug(): ?string
    {
        return $this->slug;
    }

    public function setSlug(?string $slug): self
    {
        $this->slug = $slug;

        return $this;
    }

    public function getCreatedAt(): ?\DateTimeInterface
    {
        return $this->created_at;
    }

    public function setCreatedAt(\DateTimeInterface $created_at): self
    {
        $this->created_at = $created_at;

        return $this;
    }

    public function getUpdatedAt(): ?\DateTimeInterface
    {
        return $this->updated_at;
    }

    public function setUpdatedAt(?\DateTimeInterface $updated_at): self
    {
        $this->updated_at = $updated_at;

        return $this;
    }

    public function getOperateur(): ?Operateur
    {
        return $this->operateur;
    }

    public function setOperateur(?Operateur $operateur): self
    {
        $this->operateur = $operateur;

        return $this;
    }

    /**
     * @return Collection|SousThematique[]
     */
    public function getSousthematiques(): Collection
    {
        return $this->sousthematiques;
    }

    public function addSousthematique(SousThematique $sousthematique): self
    {
        if (!$this->sousthematiques->contains($sousthematique)) {
            $this->sousthematiques[] = $sousthematique;
        }

        return $this;
    }

    public function removeSousthematique(SousThematique $sousthematique): self
    {
        $this->sousthematiques->removeElement($sousthematique);

        return $this;
    }

    /**
     * @return Collection|HoraireAction[]
     */
    public function getHoraireActions(): Collection
    {
        return $this->horaire_actions;
    }

    public function addHoraireAction(HoraireAction $horaireAction): self
    {
        if (!$this->horaire_actions->contains($horaireAction)) {
            $this->horaire_actions[] = $horaireAction;
            $horaireAction->setAction($this);
        }

        return $this;
    }

    public function removeHoraireAction(HoraireAction $horaireAction): self
    {
        if ($this->horaire_actions->removeElement($horaireAction)) {
            // set the owning side to null (unless already changed)
            if ($horaireAction->getAction() === $this) {
                $horaireAction->setAction(null);
            }
        }

        return $this;
    }

    /**
     * @return Collection|PublicAccueilli[]
     */
    public function getPublicAccueilli(): Collection
    {
        return $this->public_accueilli;
    }

    public function addPublicAccueilli(PublicAccueilli $public_accueilli): self
    {
        if (!$this->public_accueilli->contains($public_accueilli)) {
            $this->public_accueilli[] = $public_accueilli;
        }

        return $this;
    }

    public function removePublicAccueilli(PublicAccueilli $public_accueilli): self
    {
        $this->public_accueilli->removeElement($public_accueilli);

        return $this;
    }

    public function __toString()
    {
        return $this->name;
    }
}

I have been trying several solutions like:

  • Modify the latitude and longitude form fields in the ActionCrudController -->

Ex. NumberField::new('longitude')->hideOnIndex()->setNumDecimals(12),

But this is not exactly what I want because in the Detail and Edit views does not appear the exact float stored in the database. It adds 0 if a float has less than 12 decimals.

  • Another possibility I tried is modifying the Action Entity:

@ORM\Column(type="float", scale=15) private $latitude;

But it is not working either.

Do you know how can I display the latitude and longitude floats with the exact number of decimals that I have in my database in the Edit and Detail views in EasyAdmin?

Do you know where I could configure the number of decimals shown in a NumberField in EasyAdmin?

Thanks in advance for your help!

2

There are 2 answers

0
Michael On

Try adding precision (and the decimal type) to your Doctrine annotations?

/**
 * @ORM\Column(type="decimal", precision=9, scale=7)
 */
protected $latitude;
 
/**
 * @ORM\Column(type="decimal", precision=10, scale=7)
 */
protected $longitude;

You can choose how precise you need to be by tweaking them.

Reply to your most recent answer (regarding your DB error): You are getting this error because there is data in your database that is stored at a more accurate level (e.g. -120.923828919)

Precision = how many total digits (disregarding the decimal) Scale = how many digits after the decimal

MySQL requires that Scale <= Precision.

You just need to tweak your precision 8 would only allow for 8 numbers e.g. the maximum it could be would be 999.99999. Coordinates go from -90 to 90 for latitude and -180 to 180 for longitude, and it is up to you how far past the decimal point you really need. I have amended my values to be a little more generous.

Apologies, I cannot comment yet but could you try adding a custom form type option. I don't think this is down to EasyAdmin itself but more the NumberType field inherited from Symfony. Doc's seem to point to using the 'scale' form type option.

so maybe

NumberField::new('latitude')->hideOnIndex()->setFormTypeOption('scale', 8)

If you need to add more options, just use the plural version ->setFormTypeOptions(['scale' => 8]) instead.

Direct docs link to this functionality: NumberType#scale

0
ARiviere On

There's is no clean way to do this with EasyAdmin at the moment.

A quick and dirty workaround is to edit NumberConfigurator.php (EasyCorp\Bundle\EasyAdminBundle\Field\Configurator).

Replace 'fraction_digit' by 'max_fraction_digit' here:

// $formatterAttributes['fraction_digit'] = $scale;
$formatterAttributes['max_fraction_digit'] = $scale;

Then calling NumberField::new('longitude')->hideOnIndex()->setNumDecimals(12), should work as you want.

Alternatively, you can add a setMaxNumDecimals method in NumberField.php and modify sources accordingly up to NumberConfigurator.php.

(submit the merge request if you feel like it)