Howto rewrite complex query to Doctrine DBAL in TYPO3? - doctrine

I am struggling to migrate a complex SQL query to the Doctrine DBAL in TYPO3. My old repository query looks like this:
$enableFields = $GLOBALS['TSFE']->sys_page->enableFields('tx_test');
// calculate distance between geo coordinates
$distance = '';
if ($geoData) {
$distance = ', 3956 * 2 * ASIN( SQRT (POWER ( SIN((' . $geoData['latitude'] . ' - abs(tx_test.latitude)) * pi() / 180 / 2), 2) + COS(' . $geoData['latitude'] . ' * pi() / 180) * ' . 'COS(abs(tx_test.latitude)' .
' * pi() / 180) * POWER (SIN((' . $geoData['longitude'] .
' - tx_test.longitude)' .
' * pi() / 180 / 2), 2))) AS distance';
}
// General query
$sql = 'SELECT * ' . $distance .
' FROM tx_test ' .
' WHERE DATE_FORMAT(FROM_UNIXTIME(start), "%Y") = '.(int)$settings['flexformYear'].' AND published = 1 ' . $enableFields;
if($headline) {
$sql .= ' AND headline like '.$GLOBALS['TYPO3_DB']->quoteStr($headline).'%';
}
// ... and some more ...
$query = $this->createQuery();
$results = $query->statement($sql)->execute();
To migrate to Doctrine
Now I could easily remove the $GLOBALS['TYPO3_DB']->quoteStr() and replace the last two lines of the code above with this:
$connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('tx_test');
$results = $connection->executeQuery($sql)->fetchAll();
But this returns an array of the results and not the objects with the attached sub objects, eg. a FileReference object or another attached model.
Is there another way to achieve the desired result? If not, how can I secure/sanitize the user input for $headline? And do I need to write the SQL for the joined tables myself?

Here is a part of some distance calculations that should fit your use case regarding the use of Doctrine and the quoting.
Using Doctrine you'll get records as array, too. See below if you want Extbase objects.
Get the TYPO3 flavor of the Doctrine DBAL QueryBuilder:
$q = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable($table = 'tx_....');
$q->select(
...
'a.lat',
'a.lng'
)
->from($table /*, 'alias if you want' */);
I am not using the fluent interface much when doing calculations like that (although it is of course possible to do so by using any MySQL function with $q->selectLiteral()).
Parameters
To prevent SQL injection you should quote all possible user input with $q->quote() / $q->quoteIdentifier() or use parameters $q->createNamedParameter().
Constraints example
This is just a part of the constraints. It contains an example how to combine them based on conditions as is common in a search function.
if ((float)$searchObject->radiusKm > .5) {
$_radiusOrs = [
'IF (
' . $q->quoteIdentifier('lat') . ' = 0,
100000,
12742 * ASIN(
SQRT(
POWER(
SIN(
( ' . $q->quote((float)$searchObject->lat) . '
- ABS( ' . $q->quoteIdentifier('lat') . ' )
) * 0.0087266
),
2
)
+
COS( ' . $q->quote((float)$searchObject->lng) . ' * 0.01745329 ) * COS(
ABS( ' . $q->quoteIdentifier('lat') . ' ) * 0.01745329
) * POWER(
SIN(
( ' . $q->quote((float)$searchObject->lng) . '
- ' . $q->quoteIdentifier('lng') . '
) * 0.0087266
),
2
)
)
)
) < ' . $q->quote((float)$searchObject->radiusKm),
];
$q->andWhere(
$q->expr()->orX(...$_radiusOrs)
);
}
...
$aRes = $q->execute()->fetchAll();
(If you want to debug: You get the SQL with $q->getSQL(), $q->getParameters())
Mapping to Extbase objects
$dataMapper = $this->objectManager->get(\TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper::class);
$objects = $dataMapper->map(YourExtbaseModel::class, $aRes);
I would suggest: Use Extbase objects only if you have few objects or if you have enough memory and you don't care about performance. You should mostly get away with plain arrays just as good.
The Extbase query builder allowed for a little optimization when outputting into a Fluid template: passing the \TYPO3\CMS\Extbase\Persistence\Generic\QueryResult allowed to use the f:paginate ViewHelper on it which did not have to instantiate all the objects but only those shown on current page. That possiblity is not there (yet?) when using the Doctrine QueryBuilder. So using Extbase models should rather be last resort now than being the default. It seemed to be "best practice" - that is not true any more IMHO.

Related

Laravel find stores within 10KM using lat and long values

I am working on a Laravel project which has users and stores. user can add addresses with lat and
long so that while adding an address I want check that is there any store available within 10km .
stores table example values
id | name | lat | long|
1 | a |9.00 | 7.00|
1 | a |7.00 | 12.0|
and user input
{
"lat":'some value',
"long":"some value'
also some other parameters
}
I found that there is Haversine distance calculation method but I am pretty confused to how to implement it on laravel..
I know its not a good way to ask someone to write code for me but if anyone can then it will be helpful for me Thanks in Advance
You can add the following code in your model
public function ScopeDistance($query,$from_latitude,$from_longitude,$distance)
{
// This will calculate the distance in km
// if you want in miles use 3959 instead of 6371
$raw = \DB::raw('ROUND ( ( 6371 * acos( cos( radians('.$from_latitude.') ) * cos( radians( latitude ) ) * cos( radians( longitude ) - radians('.$from_longitude.') ) + sin( radians('.$from_latitude.') ) * sin( radians( latitude ) ) ) ) ) AS distance');
return $query->select('*')->addSelect($raw)->orderBy( 'distance', 'ASC' )->groupBy('distance')->having('distance', '<=', $distance);
}
And you can use it with something like this
$ads = Ad::with(['user','photos'])->distance($from_latitude,$from_longitude,$distance)->get();
This is my function that I used just recently to get nearest warehouses by distance in ASC order
public static function getNearestWarehouse($dropOffAddress)
{
return self::select('id', 'name', 'lat', 'long', DB::raw(sprintf(
'(6371 * acos(cos(radians(%1$.7f)) * cos(radians(`lat`)) * cos(radians(`long`) - radians(%2$.7f)) + sin(radians(%1$.7f)) * sin(radians(`lat`)))) AS distance',
$dropOffAddress->lat,
$dropOffAddress->long
)))
->having('distance', '<', 10)
->orderBy('distance', 'asc')
->get();
}
Just change the variables according to your needs and you are done

How check a location is in a given range or not?

I have an website where i am storing my company's location( longitude and lattitude ) in DB and i want to give an alert message( or any kind of response ) whenever an employee of my company enters within a given range(
500m ) of the company's location .
Can anybody please help me or me some idea how to do this .
it will be more helpfull if will be possible without any third party package .
Thanks .
Try this to query in geolocation range
DB::select(DB::raw("select * from (SELECT *, 6371
* ACOS(COS(RADIANS(" . $lat . "))
* COS(RADIANS(table_name.lat))
* COS(RADIANS(table_name.lon)
- RADIANS(" . $lon . "))
+ SIN(RADIANS(" . $lat . "))
* SIN(RADIANS(table_name.lat))) AS distance
FROM table WHERE status = 1 ) t where distance < 500"))
Above query there are added custom condition like status = 1 and range
from 500. When you getting data then you send notification to the users

How to "orderBy" a query with a variable parameter?

I'm working on Symfony4 with doctrine.
On my repository, i get informations but i can't do an order by with a variable parameter( distance between a user an many other users)
public function findByThemeAndDistance($theme,$distance,$user){
$latUser=$user->getAddress()->getLnt();
$lngUser = $user->getAddress()->getLgt();
return $this->createQueryBuilder('a')
// ->select('a as activity,dist(distance) as distance' )
->join('a.author','u')
->join('u.address','add')
->andWhere('a.theme=:val')
->andWhere( '(6378 * acos(cos(radians( add.lnt)) * cos(radians(' . $latUser . ')) * cos(radians(' . $lngUser . ') - radians(add.lgt)) + sin(radians(add.lnt )) * sin(radians(' . $latUser . '))))< :distance')
->setParameter('distance', $distance)
->setParameter('val',$theme)
->orderBy('distance','ASC')
->getQuery()
->getResult()
;
}
I try to get e result ordered by distance but I get This :
Error: Expected Doctrine\ORM\Query\Lexer::T_IDENTIFIER, got '1' and
when i erase the alais, I get This : [Semantical Error] line 0, col
287 near 'distance ASC': Error: 'distance' is not defined.
As the distance field does not exist on your entity you have to add the select and order by its alias
->addSelect('dist(distance) as distance')
->orderBy('distance','ASC')

Doctrine distances function

I try to use a function which calculate distance between $user lat/lng and coordinates in my BDD with limited distance.
It's an SQL request and I try to use Doctrine to implement it.
Here is my code
$config = new \Doctrine\ORM\Configuration();
$config->addCustomNumericFunction('COS', 'DoctrineExtensions\Query\Mysql\Cos');
$config->addCustomNumericFunction('ACOS', 'DoctrineExtensions\Query\Mysql\Acos');
$config->addCustomNumericFunction('RADIANS', 'DoctrineExtensions\Query\Mysql\Radians');
$config->addCustomNumericFunction('SIN', 'DoctrineExtensions\Query\Mysql\Sin');
$maxLat = $form_citylat + rad2deg($rad / $R);
$minLat = $form_citylat - rad2deg($rad / $R);
$maxLng = $form_citylng + rad2deg(asin($rad / $R) / cos(deg2rad($form_citylat)));
$minLng = $form_citylng - rad2deg(asin($rad / $R) / cos(deg2rad($form_citylat)));
$qb = $this->createQueryBuilder('u')->select('u.lat, u.lng')
->addSelect('acos(sin(:lat)*sin(radians(u.Lat)) + cos(:lat)*cos(radians(u.Lat))*cos(radians(u.lng)-:lng)) * :R As D')
->where('lat Between :minlat And :maxlat And lng Between :minlng And :maxlng And acos(sin(:lat)*sin(radians(u.Lat)) + cos(:lat)*cos(radians(u.Lat))*cos(radians(u.Lng)-:lng)) * :R < :rad')
->setParameter('lat',deg2rad($form_citylat))
->setParameter('lng',deg2rad($form_citylng))
->setParameter('minlat',$minLat)
->setParameter('minlng',$minLng)
->setParameter('maxlat',$maxLat)
->setParameter('maxlng',$maxLng)
->setParameter('rad',$rad)
->setParameter('R',$R)
->orderBy('D');
return $qb->getQuery()->getResult();`
But I got this error message :
[Syntax Error] line 0, col 40: Error: Expected
Doctrine\ORM\Query\Lexer::T_CLOSE_PARENTHESIS, got '.'
I tried different options but it doesn't work.
Anyone has an answer please ?
public function findByThemeAndDistance($theme,$distance,$user){
$latUser=$user->getAddress()->getLnt();
$lngUser = $user->getAddress()->getLgt();
return $this->createQueryBuilder('a')
//->select('a as activity,dist('.$distance.') as distance' )
->join('a.author','u')
->join('u.address','add')
->andWhere('a.theme=:val')
->andWhere( '(6378 * acos(cos(radians( add.lnt)) * cos(radians(' . $latUser . ')) * cos(radians(' . $lngUser . ') - radians(add.lgt)) + sin(radians(add.lnt )) * sin(radians(' . $latUser . '))))< :distance')
->setParameter('distance', $distance)
->setParameter('val',$theme)
->orderBy( 'a.author','ASC')
->getQuery()
->getResult();
}

Laravel 4.2 - pagination failing when using having distance as

I have a complex search query built that works well until I introduce distance calculations..
This is how I do it..
$profiles = Profile:: ... ... ...
...
...
...
...
$lat = Auth::User()->profile->latitude;
$lng = Auth::User()->profile->longitude;
$gr_circle_radius = 6371;
/*
* Generate the select field for disctance
*/
$disctance_select = sprintf(
"*, ( %d * acos( cos( radians(%s) ) " .
" * cos( radians( latitude ) ) " .
" * cos( radians( longitude ) - radians(%s) ) " .
" + sin( radians(%s) ) * sin( radians( latitude ) ) " .
") " .
") " .
"AS distance",
$gr_circle_radius,
$lat,
$lng,
$lat
);
$profiles = $profiles->select( DB::raw($disctance_select) )
->having( 'distance', '<', 80 )
->orderBy( 'distance', 'ASC' )
->paginate(12);
When I use paginate() I get this error
Column not found: 1054 Unknown column 'distance' in 'having clause'
If I use get() the query works but then I get
Call to undefined method Illuminate\Database\Eloquent\Collection::links()
.. and I would reallllyyy like o have easy pagination working.
Are there any good suggestions on how to address this. I have been searching around and found nothing useful.
Apparently there is an issue with eloquent when using having?
All help would be greatly appreciated.
If your issue is not solved then use ->simplePaginate(12) instead of >paginate(12)

Resources