【问题标题】:How to search the database for a keyword and return the data back that is located within 50 miles如何在数据库中搜索关键字并返回位于 50 英里内的数据
【发布时间】:2017-07-19 16:34:47
【问题描述】:

我已经能够在数据库中搜索关键字并将结果以 json 格式返回给应用程序,但现在我正在尝试 准备一个语句,该语句将返回位于选定英里数内的数据,但该语句返回 null。 我在 $sql = $db->prepare 语句和 $sql->execute 语句中做错了什么?

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;

}   

【问题讨论】:

  • 所有变量都需要占位符。占位符应该是唯一的,并且每个占位符都应该有一个绑定。示例WHERE businessNameDB=:word1 OR lastNameDB=:word2 OR firstKeywordDB=:word3 OR secondKeywordDB=:word4 OR thirdKeywordDB=:word5 OR fourthKeywordDB=:word6 OR fithKeywordDB=:word7 .... POWER(SIN((:latitude
  • 如果您在代码中添加了一些错误检查和错误报告,那么答案将非常明显......
  • 只有一个词被发送用于搜索,它搜索 businessNameDB=:word OR lastNameDB=:word OR firstKeywordDB=:word OR secondKeywordDB=:word OR thirdKeywordDB=:word OR FourthKeywordDB=:word OR fithKeywordDB=:word ...当我只用 word 运行搜索时,我得到了 json。这是正确的。现在添加经度和纬度和英里是我得到空值的时候。
  • 我是 php 新手,如何添加错误检查?那会有很大帮助。谢谢
  • 我今天发布了一个答案,其中包含如何正确实施 PDO 的完整示例:PDO prepared statements and exception handling in PHP。我希望它有所帮助。祝你好运!

标签: php mysql pdo


【解决方案1】:

试试这个代码。我无法测试它,但如果你一步一步地耐心等待它会起作用。如果您有错误或遇到问题,请给我们反馈。可能是,sql 语句中LIMIT 的参数在第一次尝试时不起作用。告诉我们。另外,我使用了以下代码来进行 Haversine 公式和矩形计算:Reverse Geocoding with it loaded into MySQL database?,因为它与您的几乎相同。

一些建议:

  • 尝试转到 OOP。
  • 始终应用异常处理并激活错误报告和 处理。我只抛出Exception 给了你一个大致的看法。 通常你应该学习如何扔和处理SPL (标准 PHP 库)类型。
  • 始终使用prepared statements(我知道你已经这样做了;-)。
  • 始终阅读 PHP Manual 中的 PHP 函数返回的内容,以便您可以正确应用句柄情况(异常、布尔值等)。

资源:

祝你好运!


具有 PDO 准备语句和异常处理的Haversine 公式

index.php(主页):

<?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(包含在主页中):

<?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(包含在主页中):

<?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(包含在主页中):

<?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));
}

编辑 1:针对 MySQL 注入的附加措施:

像这样申请qoute()

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

    //...
}

编辑 2:解决错误的近似值:

在您的原始代码中,您使用了cos(radians($latitude))*69

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

在我的代码中,我使用了abs(cos(deg2rad($latitude)) * 69)。我记得我是故意选择这个的:

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

看来这可能是问题所在。因此,首先,将deg2rad 替换为radians。那么它应该是:

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

如果还是不行,就删除abs。那么它应该是:

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

注意括号的位置。

编辑 3 - 可变测量单位:

应用可变测量单位(公里、英里等)作为函数参数并相应地更改地球半径。如何将$measurementUnit 注入函数是您的职责。是的,地球半径几乎是 3959 英里。

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
    );

    //...
}

【讨论】:

  • 谢谢。我开始工作了,但你怎么能停止注射呢?我的 php 程序员不知道 php,让我挂了。试图糊弄过去。
  • @dscrown 嗨,我很高兴它成功了。如果您询问有关 MySQL 注入的问题:您使用 prepared 语句的简单事实确保了用户输入的 quote()-ing - php 驱动程序自动执行此操作 - 因此确保避免注入。阅读THISTHIS 但是您的sql 语句仍然不能安全地防止注入,因为%s AS distance 部分。
  • @dscrown 在THIS,阅读第二个主要好处部分。声明:“...(但是,如果查询的其他部分是使用非转义输入构建的,SQL 注入仍然是可能的)”。因此,您必须在$latitude$longitude(或$distance)上使用quote()。阅读PDO::quote
  • @dscrown 除了准备好的语句或quote()-ing 之外,还有一些其他“步骤”可应用于用户输入。阅读SQL Injection Prevention Cheat Sheet,也请在网上搜索。主题非常重要。在不久的将来,我也必须在我自己的 MVC 项目中应用所有这些其他机制 :-) 如果您有任何(其他)问题,请随时问我和我们。祝你好运...... MySQL 注入安全! :-)
  • @dscrown 我用“EDIT 1: Additional measure against MySQL injection”重新编辑了我的答案。看看它并在整个项目中应用相同的原则,其中“查询的其他部分正在使用非转义输入构建”。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-11-09
  • 2012-06-21
  • 2014-09-02
  • 1970-01-01
相关资源
最近更新 更多