Write a Doctrine2 query to join two spatial tables - symfony

I am using Symfony with Doctrine2 on a Postgre database with PostGIS enabled. I have two tables - property and neighborhood with the following structures:
class Property {
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\Column(type="string")
*/
protected $address;
/**
* #var Point $geom
* #ORM\Column(type="Point", nullable=true)
*/
protected $geom;
}
class Neighborhood {
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $gid;
/**
* #ORM\Column(type="string")
*/
protected $name;
/**
* #ORM\Column(type="string")
*/
protected $description;
/**
* #var Polygon $geom
*
* #ORM\Column(type="Polygon", nullable=true)
*/
protected $geom;
}
In pgAdminIII, I can write the following query which works fine:
SELECT address, neighborhood.name
FROM property
JOIN neighborhood
ON ST_Contains(neighborhood.geom, property.geom)
How can I write this in DQL? I understand the basics of joins and adding the annotations for the Doctrine2 mapping, but I am not sure how to do the join since the two fields are not equal. I need to use the ST_Contains function to create the join.
I am using the djlambert / doctrine2-spatial bundle for the spatial data types and mapping. I am able to query each table individually and have created maps on each, but am not sure how to select all of the properties in a given neighborhood.

I figured this out and want to post in case someone else has a similar question. I needed to make the ST_Contains part of a condition and not just the function alone.
using the createQueryBuilder, here is the syntax:
$qb = $this->createQueryBuilder('n')
->select("p.address as address, n.ntaname, ST_AsText(p.geom) as function")
->join('Bundle\Entity\Property', 'p', 'WITH', 'ST_Contains(n.geom4326,p.geom)=true');
return $qb->getQuery()
->getResult();

This is not a real answer but just a comment on #George's code.
For me, using creof/doctrine2-spatial:0.0.1 and symfony/symfony:2.3.*, the annotation needed to be lower case for the schema to validate with php app/console doctrine:schema:validate, like this :
/**
* #var Point $geom
* #ORM\Column(type="point", nullable=true)
*/
protected $geom;
With a capital case, I kept getting this error message :
[Doctrine\DBALDBALException]
Unknown column type "Point" requested. Any Doctrine type that you use has to be registered with \Doctrine\DBAL\Types\Type::addType(). [...]

Related

Symfony/Doctrine: Accessing unmapped(?) data within entity

in Symfony (5.3.7 at present) I've got main data-entities and settings seperated. For example there is a user entity (default stuff), a TypeUserSetting defining the different settings and UserSetting which is m:n in between with the current setting stored.
namespace App\Entity;
/**
* #ORM\Entity(repositoryClass=TypeUserSettingRepository::class)
*/
class TypeUserSetting
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=200)
*/
private $description;
/**
* #ORM\OneToMany(targetEntity=UserSetting::class, mappedBy="setting")
*/
private $userSettings;
/**
* #ORM\Column(type="string", length=255, nullable=true)
*/
private $default_value;
and
class UserSetting
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=4000, nullable=true)
*/
private $value;
/**
* #ORM\ManyToOne(targetEntity=User::class, inversedBy="userSettings")
* #ORM\JoinColumn(nullable=false)
*/
private $user;
/**
* #ORM\ManyToOne(targetEntity=TypeUserSetting::class, inversedBy="userSettings",cascade={"persist"})
* #ORM\JoinColumn(nullable=false)
*/
private $setting;
Thats not that complicated so far...
My problem is, that oftenly settings don't exist, because the users did not set them. In that case I want to use the default.
class User ...
/**
* #ORM\OneToMany(targetEntity=UserSetting::class, mappedBy="user", orphanRemoval=true)
*/
private $userSettings;
...
public function getSettingById(int $id):string
{
foreach ($this->getUserSettings() as $oneSetting) {
if ($oneSetting->getId() === $id)
return $oneSetting->getValue();
}
//Not set, return the default
....
}
And here we go... If there is no setting, the loop fails and I want to get the default form the coresponding TypeUserSetting. This is not mapped, so I have to get it from the database, but I didn't find a way to access that properly.
Possible solutions I found that far:
Insert the UserSetting for all users by SQL when adding a TypeUserSetting. This would avoid the whole problem, but I simply don't like that.
Adding a static method to the TypeUserSetting-repo to get the value. I think that's ugly and somehow going back to last century...
Inject the TypeUserSetting-repo by LifeCycle-hooks which (in my oppinion) isn't the way entities should be used.
Injecting the repo from the controller that calls the function... I think this would be the opposite of encapsulation and separation of concerns. (and I think about hitting my head to the wall, just for having this kind of thoughts)
Anybody got a good idea to solve that?
Thanks in advance

Doctrine: select a table that is not managed by doctrine

Using the Doctrine QueryBuilder, I want to execute a query which in native SQL looks like this:
`SELECT image FROM image i INNER JOIN about_images a ON i.id = a.image_id`;
The result in DQL is as follows:
ImageRepository.php:
return $this->createQueryBuilder('i')
->select('i')
->innerJoin('about_images', 'a', 'WITH', 'i.id = a.imageId')
->getQuery()
->getResult();
Where image is an entity, and about_images is a join table (the result of a #ManyToMany relationship). However, I get the error that about_images is not defined, which makes sense as it is not managed by doctrine.
AboutPage.php (i.e the entity where the join table is created)
/**
* #var Image[]|ArrayCollection
*
* #ORM\ManyToMany(targetEntity="App\Entity\Image", cascade={"persist", "remove"})
* #ORM\JoinTable(name="about_images",
* joinColumns={#ORM\JoinColumn(name="about_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="image_id", referencedColumnName="id", unique=true)})
*/
private $images;
Fields from Image entity:
/**
* #var int
*
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #var string
*
* #ORM\Column(type="string", length=255)
*/
private $image;
/**
* #var File
*
* #Vich\UploadableField(mapping="collection_images", fileNameProperty="image")
* #Assert\File(maxSize="150M", mimeTypes={"image/jpeg", "image/jpg", "image/png", "image/gif"},
* mimeTypesMessage="The type ({{ type }}) is invalid. Allowed image types are {{ types }}")
*/
private $imageFile;
/**
* #var string
*
* #ORM\Column(type="string", length=255, nullable=true)
*/
private $imageAlt;
/**
* #var DateTime
*
* #ORM\Column(type="datetime")
*/
private $updatedAt;
/**
* #var string
*
* #ORM\Column(type="string", nullable=true)
*/
private $alt;
How can I solve this problem? The results should be Image entities.
You can write native SQL and then map the output to your entities using a ResultSetMapper.
For your example it could look something like this in your Repository class:
public function findImagesWithAboutImages()
{
$sql = 'SELECT i.* FROM image i INNER JOIN about_images a ON i.id = a.image_id';
$entityManager = $this->getEntityManager();
$mappingBuilder = new ResultSetMappingBuilder($entityManager);
$mappingBuilder->addRootEntityFromClassMetadata(Image::class, 'i');
$query = $entityManager->createNativeQuery($sql, $mappingBuilder);
// If you want to set parameters e.g. you have something like WHERE id = :id you can do it on this query object using setParameter()
return $query->getResult();
}
If you want related data you will have to add it to the select clause with an alias and then use $mappingBuilder->addJoinedEntityFromClassMetadata() to assign these fields to the joined entity much like above with the root entity.
Your annotations in your entity already define how each field maps to a property and what type it has, so basically you should get an array of Image-entities with everything (but the related entities) loaded usable.
It is not quite clear the example sql with the code you have provided, but if you have a relation defined in your entities, you can join them with a query builder just by telling the relation field of the entity, so I think this should work
return $this->createQueryBuilder('i')
->select('i')
->innerJoin('i.images', 'a')
->getQuery()
->getResult();
As you have defined already your relations in your entities, Doctrine knows how to join your tables, so you just have to specify the relation field name and the alias.
And always remember that you have to use the field name in your entity (normally cameCasedStyle), not the column name at your database tables (normally snake_cased_style).

Problem Symfony/Doctrine : One-To-Many - Self-referencing on a primary key

I would like to have a "post" with an identifier. This one could be classified in another "post" by storing the identifier of his parent.
I tried to do like this:
class Post {
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
* #ORM\OneToMany(targetEntity="Post", mappedBy="Id_Post_Parent")
*/
private $Id_Post;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Post", inversedBy="Id_Post")
* #ORM\JoinColumn(name="Id_Post", referencedColumnName="Id_Post", nullable=true)
*/
private $Id_Post_Parent;
...
}
but I have this error when i'm checking with doctrine:schema:validate :
[FAIL] The entity-class App\Entity\Post mapping is invalid:
The association App\Entity\Post#Id_Post_Parent refers to the inverse side field App\Entity\Post#Id_Post which is not defined as association.
The association App\Entity\Post#Id_Post_Parent refers to the inverse side field App\Entity\Post#Id_Post which does not exist.
The referenced column name 'Id_Post' has to be a primary key column on the target entity class 'App\Entity\Post'.
Can someone help me to fix this ?
There is small logical error with your structure - your ID_Post variable tries to be both the primary key (the ID) and the collection association side. I didn't check this syntax in too much details (you can find an example of this association along with most of the other associations from doctrine documentation: https://www.doctrine-project.org/projects/doctrine-orm/en/2.6/reference/association-mapping.html#one-to-many-self-referencing), but basically you need to add the children association separately to your entity like this:
class Post
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity="Post", inversedBy="postChildren")
* #ORM\JoinColumn(name="id_parent_post", referencedColumnName="id", nullable=true)
*/
private $postParent;
/**
* #ORM\OneToMany(targetEntity="Post", mappedBy="postParent")
*/
private $postChildren;
public function __construct() {
$this->postChildren = new \Doctrine\Common\Collections\ArrayCollection();
}
}

Symfony2 entity foreign keys

I can't figure it out..
Why I haven't access to Country table?
countryName should show Great Britain but it doesn't.
This is my dump($User):
My my piece of code of User entity:
/**
*
* #ORM\ManyToOne(targetEntity="Dashboard\MainBundle\Entity\Country", cascade={"persist"})
* #ORM\JoinColumn(name="country_id", referencedColumnName="id", nullable=true)
*
*/
private $countryId;
And my piece of code of Country Entity:
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
Depending on how you get the user maybe it is a lazy load that you are using which will get the country only if you call the getter explicitly, to always get a country with the user try :
/**
*
* #ORM\ManyToOne(targetEntity="Dashboard\MainBundle\Entity\Country", cascade={"persist"}, fetch="EAGER")
* #ORM\JoinColumn(name="country_id", referencedColumnName="id", nullable=true)
*
*/
private $countryId;
But still we need to know how you are getting the user the lazy load may override the fetch eager.
Your Country object is now only a Proxy object - dump function don't call a Doctrine to get a related object. Try before dump get your object for example:
dump($User->getCountry()):
dump($User);
OR try left join you Country in QueryBuilder
OR find a information about lazy load in Doctrine2 here

Entity containing other entities without having a table

I have an Entity ( Invoice ) which is purely for calculation purposes and has no table, that associates with two other entities having relations by tables. (Although there are so many other entities involved ).
class Row{
/**
* #var integer
*
* #ORM\Column(name="row_id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity="File")
* #ORM\JoinColumn(name="file_id", referencedColumnName="file_id")
*/
protected $file;
/**
* #var \DateTime
*
* #ORM\Column(name="date", type="date")
*/
private $date;
}
class File
{
/**
* #var integer
*
* #ORM\Column(name="file_id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="name", type="string", length=255)
*/
private $name;
}
class Invoice
{
/**
* #ORM\Id
* #ORM\Column(name="invoice_id", type="integer")
* #ORM\GeneratedValue
*/
protected $id = null;
/**
* #ORM\OneToMany(targetEntity="Row", mappedBy="row_id")
*/
protected $row;
/**
* #ORM\OneToMany(targetEntity="File", mappedBy="file_id")
*/
protected $file;
}
I want to be able to query for Invoices :
$sDate = //Some date
$this->getEntityManager()
->createQuery("SELECT Invoice, Row, File
FROM
ReportsEntitiesBundle:Invoice Invoice
LEFT JOIN
Row.row Row
LEFT JOIN
Row.file File
WHERE date=:date"
)
->setParaMeter(':date', $sDate)
->setFirstResult($iPage*$iLimit)
->setMaxResults($iLimit)
->getResult();
The questions :
# Doctrine tries to query the database, how can I prevent that and have it find the relevant entities?
# How can I relate the date ( which is in Row entity and cannot be in Invoice ) to the query?
Later this Invoice will become a part of another big entity for calculating/search purposes.
Thank you
Short Answer: You can't
Long Answer : You can't because an entity with #ORM annotations means its persisted to a database - querying that entity relates to querying a database table. Why not just create the table ?!?!?
You need somewhere to persist the association between file and row - a database table is a perfect place !!!!
Update
Just to clarify ... an Entity is just a standard class - it has properties and methods ... just like any other class - When you issue doctrine based commands it uses the annotations within the entities to configure the tables / columns / relationships etc if remove those you can use it however you like ... but you will need to populate the values to use it and you wont be able to use it in a Doctrine query and it obviously wont be persisted !
You can use a read-only entity. It's contents are backed by a view which you create manually in SQL.
PHP:
/** #ORM\Entity(readOnly =true) */
class InvoiceView
{ ...
SQL:
CREATE VIEW invoice_view AS (
SELECT ...

Resources