Advanced field value conversion using custom mapping types
When creating entities, you sometimes have the need to transform field values before they are saved to the database. In Doctrine you can use Custom Mapping Types to solve this <see: Quoting Reserved Words>.
There are several ways to achieve this: converting the value inside the Type class, converting the value on the database-level or a combination of both.
This article describes the third way by implementing the MySQL specific column type Point.
The Point
type is part of the Spatial extension
of MySQL and enables you to store a single location in a coordinate space by
using x and y coordinates. You can use the Point type to store a
longitude/latitude pair to represent a geographic location.
The entity
We create a simple entity with a field $point
which holds a value object
Point
representing the latitude and longitude of the position.
The entity class:
1 <?php
namespace Geo\Entity;
use Geo\ValueObject\Point;
#[Entity]
class Location
{
#[Column(type: 'point')]
private Point $point;
#[Column]
private string $address;
public function setPoint(Point $point): void
{
$this->point = $point;
}
public function getPoint(): Point
{
return $this->point;
}
public function setAddress(string $address): void
{
$this->address = $address;
}
public function getAddress(): string
{
return $this->address;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
We use the custom type point
in the #[Column]
attribute of the
$point
field. We will create this custom mapping type in the next chapter.
The point class:
1 <?php
namespace Geo\ValueObject;
class Point
{
public function __construct(
private float $latitude,
private float $longitude,
) {
}
public function getLatitude(): float
{
return $this->latitude;
}
public function getLongitude(): float
{
return $this->longitude;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
The mapping type
Now we're going to create the point
type and implement all required methods.
1 <?php
namespace Geo\Types;
use Doctrine\DBAL\Types\Type;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Geo\ValueObject\Point;
class PointType extends Type
{
const POINT = 'point';
public function getName()
{
return self::POINT;
}
public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
{
return 'POINT';
}
public function convertToPHPValue($value, AbstractPlatform $platform)
{
list($longitude, $latitude) = sscanf($value, 'POINT(%f %f)');
return new Point($latitude, $longitude);
}
public function convertToDatabaseValue($value, AbstractPlatform $platform)
{
if ($value instanceof Point) {
$value = sprintf('POINT(%F %F)', $value->getLongitude(), $value->getLatitude());
}
return $value;
}
public function canRequireSQLConversion()
{
return true;
}
public function convertToPHPValueSQL($sqlExpr, AbstractPlatform $platform)
{
return sprintf('AsText(%s)', $sqlExpr);
}
public function convertToDatabaseValueSQL($sqlExpr, AbstractPlatform $platform)
{
return sprintf('PointFromText(%s)', $sqlExpr);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
We do a 2-step conversion here. In the first step, we convert the Point
object into a string representation before saving to the database (in the
convertToDatabaseValue
method) and back into an object after fetching the
value from the database (in the convertToPHPValue
method).
The format of the string representation format is called Well-known text (WKT). The advantage of this format is, that it is both human readable and parsable by MySQL.
Internally, MySQL stores geometry values in a binary format that is not identical to the WKT format. So, we need to let MySQL transform the WKT representation into its internal format.
This is where the convertToPHPValueSQL
and convertToDatabaseValueSQL
methods come into play.
This methods wrap a sql expression (the WKT representation of the Point) into MySQL functions ST_PointFromText and ST_AsText which convert WKT strings to and from the internal format of MySQL.
When using DQL queries, the If you want to use Point values in WHERE clauses, you have to implement a
user defined function for
|
Example usage
1 <?php
// Bootstrapping stuff...
// $em = new \Doctrine\ORM\EntityManager($connection, $config);
// Setup custom mapping type
use Doctrine\DBAL\Types\Type;
Type::addType('point', 'Geo\Types\PointType');
$em->getConnection()->getDatabasePlatform()->registerDoctrineTypeMapping('point', 'point');
// Store a Location object
use Geo\Entity\Location;
use Geo\ValueObject\Point;
$location = new Location();
$location->setAddress('1600 Amphitheatre Parkway, Mountain View, CA');
$location->setPoint(new Point(37.4220761, -122.0845187));
$em->persist($location);
$em->flush();
$em->clear();
// Fetch the Location object
$query = $em->createQuery("SELECT l FROM Geo\Entity\Location l WHERE l.address = '1600 Amphitheatre Parkway, Mountain View, CA'");
$location = $query->getSingleResult();
/** @var Geo\ValueObject\Point */
$point = $location->getPoint();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30