0

I have been able to search the database for a keyword and return the result back as json to the app, but now I am trying to prepare a statement that will returns the data that is located within a selected number of miles but this statement return null. What am I doing wrong in the $sql = $db->prepare statement and the $sql->execute statement?

FUNCTION TO GET CARD FROM SEARCH WORD, LONGITUDE, LATITUDE, AMD MILES CALLED FROM GetCards.php   
public function getAllCards($word, $longitude, $latitude, $miles) {

//Connect to db using the PDO not PHP
$db = new PDO('mysql:host=localhost;dbname=XXXXX', 'XXXXX', 'XXXXX');

//Here we prepare the SELECT statement from the search, word place holder :word, longitude place holder :longitude, latitude place holder :latitude, miles place holder :miles
$sql = $db->prepare('SELECT * FROM carddbtable WHERE businessNameDB=:word OR lastNameDB=:word OR firstKeywordDB=:word OR secondKeywordDB=:word OR thirdKeywordDB=:word OR fourthKeywordDB=:word OR fithKeywordDB=:word AND latitudeLocateDB, longitudeLocateDB, 3956 * 2 * 
      ASIN(SQRT( POWER(SIN(($latitude - latitudeLocateDB)*pi()/180/2),2)
      +COS($latitude*pi()/180 )*COS(latitudeLocateDB*pi()/180)
      *POWER(SIN(($longitude-longitudeLocateDB)*pi()/180/2),2))) 
      as distance FROM $carddbtable WHERE 
      longitudeLocateDB between ($longitude-$miles/cos(radians($latitude))*69) 
      and ($longitude+$miles/cos(radians($latitude))*69) 
      and latitudeLocateDB between ($latitude-($miles/69)) 
      and ($latitude+($miles/69)) 
      having distance < $miles ORDER BY distance limit 100');


//We execute the $sql with the search word variable $word, $longitude, $latitude, $miles
$sql->execute([':word' => $word, ':longitude' => $longitude, ':latitude' => $latitude, ':miles' => $miles]);

//Looping through the results
while ($row = $sql->fetch(PDO::FETCH_ASSOC)) {

//Store all return rows in $returnArray
$returnArray[] = $row;

//Print to screen
// echo json_encode($row). "<br>"."<br>";
}

//Feedback results
return $returnArray;

}   
dscrown
  • 578
  • 1
  • 5
  • 21
  • 1
    All the variables need to be placeholders. Placeholders should be unique, and you should have a binding for each placeholder. Example `WHERE businessNameDB=:word1 OR lastNameDB=:word2 OR firstKeywordDB=:word3 OR secondKeywordDB=:word4 OR thirdKeywordDB=:word5 OR fourthKeywordDB=:word6 OR fithKeywordDB=:word7 .... POWER(SIN((:latitude` – chris85 Jul 19 '17 at 16:37
  • Had you added some error checking and error reporting to your code, then answer would be quite obvious... – Shadow Jul 19 '17 at 16:39
  • There is only one word sent for the search and it searches businessNameDB=:word OR lastNameDB=:word OR firstKeywordDB=:word OR secondKeywordDB=:word OR thirdKeywordDB=:word OR fourthKeywordDB=:word OR fithKeywordDB=:word ... When I run a search only with word I get back json. This was working correct. Now adding in the longitude and latitude and miles is when I get null. – dscrown Jul 19 '17 at 16:47
  • I am so new to php how do I add error checking? That would help a lot. Thanks – dscrown Jul 19 '17 at 16:50
  • I posted an answer today, containing a complete example of how to correctly implement PDO: [PDO prepared statements and exception handling in PHP](https://stackoverflow.com/questions/45183736/php-mysql-pdo-query-fill-up-variable-with-query-result/45184628#45184628). I hope it helps. Good luck! –  Jul 19 '17 at 17:30
  • Hi. Have you found your solution? Can I be of any further help? –  Jul 20 '17 at 08:07

1 Answers1

1

Give it a try with this code. I couldn't test it, but if you go step by step and have patience it will work. If you have errors or encounter problems, give us feedback. It may be, that the parameter for LIMIT in sql statement will not work from the first try. Just tell us. Also, I used the following code for the Haversine formula and rectangle calculation: Reverse Geocoding with it loaded into MySQL database?, because it's almost identical with yours.

Some recommendations:

  • Try to move to OOP.
  • Always apply exception handling and activate error reporting and handling. I gave you a general view by throwing only Exception. Normally you should learn how to throw and handle the SPL (Standard PHP Library) types of it too.
  • Always use prepared statements (I know you're already doing this ;-).
  • Always read in the PHP Manual what PHP functions are returning so that you can correctly apply handle cases (exceptions, bools, etc).

Resources:

Good luck!


Haversine formula with PDO prepared statements & exception handling

index.php (main page):

<?php

require_once 'configs.php';
require_once 'functions.php';
require_once 'geolocationFunctions.php';

// Activate error reporting (only on development).
activateErrorReporting();

try {
    // Create db connection.
    $connection = createConnection(
            MYSQL_HOST
            , MYSQL_DATABASE
            , MYSQL_USERNAME
            , MYSQL_PASSWORD
            , MYSQL_PORT
            , MYSQL_CHARSET
    );

    $cardsInRadius = getCardsInRadius($connection, $word, $longitude, $latitude, $radius, $limit = 100);

    // For testing purposes.
    printData($cardsInRadius, TRUE);

    closeConnection($connection);
} catch (PDOException $pdoException) {
    // On development.
    printData($pdoException, TRUE);

    // On production.
    // echo $pdoException->getMessage();

    exit();
} catch (Exception $exception) {
    // On development.
    printData($exception, TRUE);

    // On production.
    // echo $exception->getMessage();

    exit();
}

geolocationFunctions.php (included in main page):

<?php

/*
 * ---------------------
 * Geolocation functions
 * ---------------------
 */

/**
 * Search the for a keyword and return data located within given radius.
 * 
 * @param PDO $connection Connection instance.
 * @param string $word Keyword to lookup.
 * @param double $longitude Longitude value to lookup.
 * @param double $latitude Latitude value to lookup.
 * @param integer $radius Distance radius (in miles) having lat/long as center point.
 * @param integer $limit [optional] Number of records to return.
 * @throws Exception
 */
function getCardsInRadius($connection, $word, $longitude, $latitude, $radius, $limit = 100) {
    /*
     * Create a rectangle in which the circle with the given radius will be defined.
     *  >> 1° of latitude ~= 69 miles
     *  >> 1° of longitude ~= cos(latitude) * 69
     */
    $rectLong1 = $longitude - $radius / abs(cos(deg2rad($latitude)) * 69);
    $rectLong2 = $longitude + $radius / abs(cos(deg2rad($latitude)) * 69);
    $rectLat1 = $latitude - ($radius / 69);
    $rectLat2 = $latitude + ($radius / 69);

    // Approximate the circle inside the rectangle.
    $distance = sprintf('3956 * 2 * ASIN(SQRT(POWER(SIN((%s - latitudeLocateDB) * pi()/180 / 2), 2) + COS(%s * pi()/180) * COS(latitudeLocateDB * pi()/180) * POWER(SIN((%s - longitudeLocateDB) * pi()/180 / 2), 2) ))'
            , $latitude
            , $latitude
            , $longitude
    );

    // Sql statement.
    $sql = sprintf('SELECT 
                        *,
                        %s AS distance 
                    FROM carddbtable 
                    WHERE 
                        (
                            businessNameDB = :businessNameDB 
                            OR lastNameDB = :lastNameDB 
                            OR firstKeywordDB = :firstKeywordDB 
                            OR secondKeywordDB = :secondKeywordDB 
                            OR thirdKeywordDB = :thirdKeywordDB 
                            OR fourthKeywordDB = :fourthKeywordDB 
                            OR fithKeywordDB = :fithKeywordDB 
                        )
                        AND longitudeLocateDB BETWEEN :rectLong1 AND :rectLong2 
                        AND latitudeLocateDB BETWEEN :rectLat1 AND :rectLat2 
                    HAVING distance < :distance 
                    ORDER BY distance 
                    LIMIT :limit'
            , $distance
    );

    // Prepare and check sql statement (returns PDO statement).
    $statement = $connection->prepare($sql);
    if (!$statement) {
        throw new Exception('The SQL statement can not be prepared!');
    }

    // Bind values to sql statement parameters.
    $statement->bindValue(':businessNameDB', $word, getInputParameterDataType($word));
    $statement->bindValue(':lastNameDB', $word, getInputParameterDataType($word));
    $statement->bindValue(':firstKeywordDB', $word, getInputParameterDataType($word));
    $statement->bindValue(':secondKeywordDB', $word, getInputParameterDataType($word));
    $statement->bindValue(':thirdKeywordDB', $word, getInputParameterDataType($word));
    $statement->bindValue(':fourthKeywordDB', $word, getInputParameterDataType($word));
    $statement->bindValue(':fithKeywordDB', $word, getInputParameterDataType($word));
    $statement->bindValue(':rectLong1', $rectLong1, getInputParameterDataType($rectLong1));
    $statement->bindValue(':rectLong2', $rectLong2, getInputParameterDataType($rectLong2));
    $statement->bindValue(':rectLat1', $rectLat1, getInputParameterDataType($rectLat1));
    $statement->bindValue(':rectLat2', $rectLat2, getInputParameterDataType($rectLat2));
    $statement->bindValue(':distance', $radius, getInputParameterDataType($radius));
    $statement->bindValue(':limit', $limit, getInputParameterDataType($limit));

    // Execute and check PDO statement.
    if (!$statement->execute()) {
        throw new Exception('The PDO statement can not be executed!');
    }

    // Fetch person details.
    $fetchedData = $statement->fetchAll(PDO::FETCH_ASSOC);
    if (!$fetchedData) {
        throw new Exception('Fetching data failed!');
    }

    return $fetchedData;
}

configs.php (included in main page):

<?php

/*
 * ----------------
 * Database configs
 * ----------------
 */

define('MYSQL_HOST', '...');
define('MYSQL_PORT', '3306');
define('MYSQL_DATABASE', '...');
define('MYSQL_CHARSET', 'utf8');
define('MYSQL_USERNAME', '...');
define('MYSQL_PASSWORD', '...');

functions.php (included in main page):

<?php

/*
 * ---------------------
 * Data access functions
 * ---------------------
 */

/**
 * Create a new db connection.
 * 
 * @param string $host Host.
 * @param string $dbname Database name.
 * @param string $username Username.
 * @param string $password Password.
 * @param string $port [optional] Port.
 * @param array $charset [optional] Character set.
 * @param array $options [optional] Driver options.
 * @return PDO Db connection.
 */
function createConnection($host, $dbname, $username, $password, $port = '3306', $charset = 'utf8', $options = array(
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_EMULATE_PREPARES => false,
    PDO::ATTR_PERSISTENT => true,
)) {
    $dsn = getDsn($host, $dbname, $port, $charset);
    $connection = new PDO($dsn, $username, $password);
    foreach ($options as $key => $value) {
        $connection->setAttribute($key, $value);
    }
    return $connection;
}

/**
 * Create a mysql DSN string.
 * 
 * @param string $host Host.
 * @param string $dbname Database name.
 * @param string $port [optional] Port.
 * @param array $charset [optional] Character set.
 * @return string DSN string.
 */
function getDsn($host, $dbname, $port = '3306', $charset = 'utf8') {
    $dsn = sprintf('mysql:host=%s;port=%s;dbname=%s;charset=%s'
            , $host
            , $port
            , $dbname
            , $charset
    );
    return $dsn;
}

/**
 * Close a db connection.
 * 
 * @param PDO $connection Db connection.
 * @return void
 */
function closeConnection($connection) {
    $connection = NULL;
}

/**
 * Get the data type of a binding value.
 * 
 * @param mixed $value Binding value.
 * @return mixed Data type of the binding value.
 */
function getInputParameterDataType($value) {
    $dataType = PDO::PARAM_STR;
    if (is_int($value)) {
        $dataType = PDO::PARAM_INT;
    } elseif (is_bool($value)) {
        $dataType = PDO::PARAM_BOOL;
    }
    return $dataType;
}

/*
 * ---------------
 * Print functions
 * ---------------
 */

/**
 * Print data on screen.
 * 
 * @param mixed $data Data to print.
 * @param bool $preformatted Print preformatted if TRUE, print normal otherwise.
 * @return void
 */
function printData($data, $preformatted = FALSE) {
    if ($preformatted) {
        echo '<pre>' . print_r($data, true) . '</pre>';
    } else {
        echo $data;
    }
}

/*
 * -------------------------
 * Error reporting functions
 * -------------------------
 */

/**
 * Toggle error reporting.
 * 
 * @param integer $level Error level.
 * @param bool $display_errors Display errors if TRUE, hide them otherwise.
 * @return void
 */
function activateErrorReporting($level = E_ALL, $display_errors = TRUE) {
    error_reporting($level);
    ini_set('display_errors', ($display_errors ? 1 : 0));
}

EDIT 1: Additional measure against MySQL injection:

Apply qoute() like this:

function getCardsInRadius($connection, ...) {
    $longitude = $connection->quote($longitude);
    $latitude = $connection->quote($latitude);

    //...
}

EDIT 2: Resolve bad approximations:

In your original code you have used cos(radians($latitude))*69:

...
between ($longitude-$miles/cos(radians($latitude))*69) 
    and ($longitude+$miles/cos(radians($latitude))*69)
...

In my code I used abs(cos(deg2rad($latitude)) * 69). As I recall I choosed this deliberately:

$rectLong1 = $longitude - $radius / abs(cos(deg2rad($latitude)) * 69);
$rectLong2 = $longitude + $radius / abs(cos(deg2rad($latitude)) * 69);

It seems that this could be the problem. So, first, replace deg2rad with radians. It should then be:

$rectLong1 = $longitude - $radius / abs(cos(radians($latitude)) * 69);
$rectLong2 = $longitude + $radius / abs(cos(radians($latitude)) * 69);

If it still doesn't work, then remove abs. It should then be:

$rectLong1 = $longitude - $radius / (cos(radians($latitude)) * 69);
$rectLong2 = $longitude + $radius / (cos(radians($latitude)) * 69);

Be aware of the parenthesis positions.

EDIT 3 - Variable measurement unit:

Apply a variable measurement unit (km, miles, etc) as function parameter and change the earth radius correspondingly. How you inject the $measurementUnit into the function is your part. Yes, the earth radius is almost 3959 miles.

function getCardsInRadius($connection, $word, $longitude, $latitude, $radius, $limit = 100, $measurementUnit = 'miles') {

    //...

    switch ($measurementUnit) {
        case 'miles':
            $earthRadius = 3959;
            break;

        case 'km':
            $earthRadius = 6371;
            break;

        default: // miles
            $earthRadius = 3959;
            break;
    }

    $distance = sprintf('%s * 2 * ...'
            , $earthRadius
            , $latitude
            , $latitude
            , $longitude
    );

    //...
}
  • Thanks. I got to working but how would you stop injection in this? My php programmer didn't know php and left me hanging. Trying to muddle my way through it. – dscrown Jul 23 '17 at 14:43
  • @dscrown Hi, I'm glad it worked. If you're asking about MySQL injection: the simple fact that you are using `prepared` statements ensures the `quote()`-ing of the user input - the php driver is doing this automatically - and therefore ensures the avoiding of injection. Read [THIS](https://stackoverflow.com/questions/60174/how-can-i-prevent-sql-injection-in-php) and [THIS](http://php.net/manual/en/pdo.prepared-statements.php) But your sql statement is still NOT safe against injection, because of the part `%s AS distance`. –  Jul 23 '17 at 18:54
  • @dscrown In [THIS](http://php.net/manual/en/pdo.prepared-statements.php), read the second major benefit part. There is stated: "_...(however, if other portions of the query are being built up with unescaped input, SQL injection is still possible)_". Therefore you must use `quote()` on the `$latitude` and `$longitude` (or on `$distance`). Read [PDO::quote](http://php.net/manual/en/pdo.quote.php). –  Jul 23 '17 at 19:01
  • @dscrown There are some other "steps" to apply on user input, other than prepared statements or `quote()`-ing, as well. Read [SQL Injection Prevention Cheat Sheet](https://www.owasp.org/index.php/SQL_Injection_Prevention_Cheat_Sheet) and please search in web about it too. The subject is very important. In the near future I'll have to apply all these other mechanismus in my own MVC project too :-) If you have any (other) questions, feel free to ask me and us. Good luck and... be MySQL injection safe! :-) –  Jul 23 '17 at 19:15
  • @dscrown I reedited my answer with "**EDIT 1: Additional measure against MySQL injection**". Take a look at it and apply the same principle all over your project, where "_other portions of the query are being built up with unescaped input_". –  Jul 23 '17 at 19:29
  • I have been doing some testing and if I search within 230 miles I get results from 268 miles. I changed the 69 to use 69.05482 I know this wouldn't make much difference but changed it any way. I can't find the answer but still looking. – dscrown Jul 24 '17 at 19:22
  • @dscrown I have a thought. I'll reedit my answer with EDIT 2 now, ok? –  Jul 24 '17 at 19:36
  • I thank I got it it seems to be distance as the crow flies. I didn't know that. – dscrown Jul 24 '17 at 19:51
  • @dscrown I finished reediting with "EDIT 2". Keep me updated. Thanks. –  Jul 24 '17 at 19:53
  • @dscrown As the crow flies? :-) –  Jul 24 '17 at 19:54
  • Literal shortest distance to your destination. Measuring geodesic distance—that's distance as the crow flies, or the literal shortest route between two points – dscrown Jul 24 '17 at 19:56
  • @dscrown Oh, I see. Interesting. –  Jul 24 '17 at 20:03
  • How could I change the 3959 to a variable in this line? $distance = sprintf('3959 * 2 * ASIN(SQRT(POWER(SIN((%s - latitudeLocateDB) * pi()/180 / 2), 2) + COS(%s * pi()/180) * COS(latitudeLocateDB * pi()/180) * POWER(SIN((%s - longitudeLocateDB) * pi()/180 / 2), 2) ))' I am trying to let the user of the app change from miles to km – dscrown Jul 24 '17 at 21:00
  • @dscrown Shouldn't it be 3956 instead of 3959 ? –  Jul 24 '17 at 21:04
  • Ok I must of hit the wrong number key earlier. I can't get the variable to take the number's place. – dscrown Jul 24 '17 at 21:10
  • Thank You, Thank You. Typo's will drive me crazy! I put in an extra dollar sign on my variable. It's amazing how hard that was to find! Thanks – dscrown Jul 24 '17 at 22:32