LoginSignup
4

More than 5 years have passed since last update.

PHPで地図とかの空間情報をDBに格納しようとしてSpatialiteを使うと苦労する

Last updated at Posted at 2015-02-04

目的

この記事ではPHPでSpatialiteを使う方法について解説します。

Spatialiteのビルドなどについては下記を参考にしてください。

地図とかの空間情報をSQLiteに格納するSpatiaLiteを使用してみる
http://qiita.com/mima_ita/items/64f6c2b8bb47c4b5b391

最終目的としては以下のコードが動作することを目標とします。

<?php

class MyDB extends SQLite3
{
    function __construct()
    {
        $this->open(':memory:');
    }
}

$ver = SQLite3::version();
var_dump($ver);


$db = new MyDB();
if (!$db->loadExtension('mod_spatialite.dll')) {
  exit();
}


# reporting some version info
$rs = $db->query('SELECT sqlite_version()');
while ($row = $rs->fetchArray())
{
  print "SQLite version: $row[0]" . PHP_EOL;
}

$rs = $db->query('SELECT spatialite_version()');
while ($row = $rs->fetchArray())
{
  print "SpatiaLite version: $row[0]" . PHP_EOL;
}

$db->exec('SELECT InitSpatialMetaData()');
$db->exec('CREATE TABLE foo (name TEXT)');
$db->exec('SELECT AddGeometryColumn("foo", "Geometry", 0, "POINT", 2)');

#SQLite 3.7.17以降じゃないとRTreeインデックスは作れない
if ($ver->versionNumber < 3007017) {
  print "RTree-Index is Not supported. (< 3.7.17)" . PHP_EOL;
} else {
  $db->exec('SELECT CreateSpatialIndex("foo", "Geometry")');
}
$db->exec('INSERT INTO foo VALUES("test1", GeometryFromText("POINT(140 30)"))');
$db->exec('INSERT INTO foo VALUES("test2", GeometryFromText("POINT(135 33)"))');

$rs = $db->query('SELECT name, AsGeoJson("Geometry") AS geo FROM foo');

while ($row = $rs->fetchArray())
{
  var_dump($row);
}

$db->close();


?>

前提

PHP5.5以降でないとloadExtensionは動作しません。
少なくとも、PHP5.3では基本無理(※)で、PHP5.6ではいけました。
なので、特に制約がなければPHPの最新を用意してください。

なお、基本無理というのは、機能を落として、なんやかんやすれば、なんとかなりはします。

あと、Windowsユーザの人は苦労します。LinuxとかMacのパスの区切りが"/"の環境だと比較的楽です。

php.iniの修正

loadExtensionを用いて拡張モジュールを使う場合、php.iniを修正して、拡張モジュールを読み込めるようにする必要があります。

Windowsの場合はデフォルトでSQLite3が無効になっている場合もあるのでphp.iniのコメントアウトを外してください。

php.ini
extension=php_sqlite3.dll

次に拡張モジュールを格納しているフォルダを指定します。

php.ini
[sqlite3]
sqlite3.extension_dir =C:\tool\spatialite\mod_spatialite-4.2.0-win-x86

PHPのSQLiteでは、ここで指定したフォルダ以外から拡張モジュールをロードすることはできません。

あとは環境変数のPATHに上記のフォルダを指定すれば、ラッキーガイな環境では動作します。お疲れ様でした。

以下のエラーが出る場合は、ここからが本当の地獄です。

PHP Warning:  SQLite3::loadExtension(): 指定されたプロシージャが見つかりません。

何でエラーになるか

おそらく、ここでエラーになるのはPHPが使用しているSQLiteのバージョンが古いか、Windowsユーザの人だと思います。

なぜエラーになるかの前にSQLiteの拡張モジュールのエントリーポイントが関係しています。

Run-Time Loadable Extensions
http://www.sqlite.org/loadext.html

本来のSQLiteのload_extensionはload_extension(X,Y)となっており、2つの引数を取ります。Xがモジュール名でYがエントリーポイントの関数名となります。
このYは省略可能で、PHPのloadExtension関数の場合、省略して実行されています。

この省略された場合の挙動は次のようになります。
・もし省略されている場合は sqlite3_extension_init を使用する
・これでロードできない場合は、最後の"/" 以降を使用する。この時、接頭語のlibは除去し、アルファベットのみを小文字にし、拡張子は考慮しない。
具体的には次のようになります。

"/usr/lib/libmathfunc-4.8.so" ⇒ "sqlite3_mathfunc_init".
"./SpellFixExt.dll" ⇒"sqlite3_spellfixext_init"

なお、mod_spatialite.dllのエントリーポイントは「sqlite3_modspatialite_init」となっています。

これについては、Dependency.Walkerで調べるかソースコード読んでください。

spatialite.png
http://www.dependencywalker.com/

つまり、PHPからloadExtensionを行った場合にエントリーポイントを「sqlite3_modspatialite_init」に変換できていないのが、このエラーの原因となります。

何故、古いPHPではsqlite3_modspatialite_initというエントリーポイントを取得できないか

PHP5.4以前の場合は話は簡単です。古いSQLiteの仕様にはこの機能がなかったものと推測できます。
実際、PHP5.4にバンドルされているSQLiteのコードに、該当の処理は存在しません。

static int sqlite3LoadExtension(
  sqlite3 *db,          /* Load the extension into this database connection */
  const char *zFile,    /* Name of the shared library containing extension */
  const char *zProc,    /* Entry point.  Use "sqlite3_extension_init" if 0 */
  char **pzErrMsg       /* Put error message here if not 0 */
){
// 略
  if( zProc==0 ){
    zProc = "sqlite3_extension_init";
  }

  handle = sqlite3OsDlOpen(pVfs, zFile);
  if( handle==0 ){
    if( pzErrMsg ){
      *pzErrMsg = zErrmsg = sqlite3_malloc(nMsg);
      if( zErrmsg ){
        sqlite3_snprintf(nMsg, zErrmsg, 
            "unable to open shared library [%s]", zFile);
        sqlite3OsDlError(pVfs, nMsg-1, zErrmsg);
      }
    }
    return SQLITE_ERROR;
  }
  xInit = (int(*)(sqlite3*,char**,const sqlite3_api_routines*))
                   sqlite3OsDlSym(pVfs, handle, zProc);
  if( xInit==0 ){
    if( pzErrMsg ){
      *pzErrMsg = zErrmsg = sqlite3_malloc(nMsg);
      if( zErrmsg ){
        sqlite3_snprintf(nMsg, zErrmsg,
            "no entry point [%s] in shared library [%s]", zProc,zFile);
        sqlite3OsDlError(pVfs, nMsg-1, zErrmsg);
      }
      sqlite3OsDlClose(pVfs, handle);
    }
    return SQLITE_ERROR;
  }else if( xInit(db, &zErrmsg, &sqlite3Apis) ){
    if( pzErrMsg ){
      *pzErrMsg = sqlite3_mprintf("error during initialization: %s", zErrmsg);
    }
    sqlite3_free(zErrmsg);
    sqlite3OsDlClose(pVfs, handle);
    return SQLITE_ERROR;
  }

見てわかる通り、sqlite3OsDlSymでxInitが取得できなかった場合にエラーとして返しています。

一方、PHP5.5では、xInitが取得できなかった場合に、代替のエントリーポイントを取得する処理が記述されています。

static int sqlite3LoadExtension(
  sqlite3 *db,          /* Load the extension into this database connection */
  const char *zFile,    /* Name of the shared library containing extension */
  const char *zProc,    /* Entry point.  Use "sqlite3_extension_init" if 0 */
  char **pzErrMsg       /* Put error message here if not 0 */
){
// 略
  xInit = (int(*)(sqlite3*,char**,const sqlite3_api_routines*))
                   sqlite3OsDlSym(pVfs, handle, zEntry);

  /* If no entry point was specified and the default legacy
  ** entry point name "sqlite3_extension_init" was not found, then
  ** construct an entry point name "sqlite3_X_init" where the X is
  ** replaced by the lowercase value of every ASCII alphabetic 
  ** character in the filename after the last "/" upto the first ".",
  ** and eliding the first three characters if they are "lib".  
  ** Examples:
  **
  **    /usr/local/lib/libExample5.4.3.so ==>  sqlite3_example_init
  **    C:/lib/mathfuncs.dll              ==>  sqlite3_mathfuncs_init
  */
  if( xInit==0 && zProc==0 ){
    int iFile, iEntry, c;
    int ncFile = sqlite3Strlen30(zFile);
    zAltEntry = sqlite3_malloc(ncFile+30);
    if( zAltEntry==0 ){
      sqlite3OsDlClose(pVfs, handle);
      return SQLITE_NOMEM;
    }
    memcpy(zAltEntry, "sqlite3_", 8);
    for(iFile=ncFile-1; iFile>=0 && zFile[iFile]!='/'; iFile--){}
    iFile++;
    if( sqlite3_strnicmp(zFile+iFile, "lib", 3)==0 ) iFile += 3;
    for(iEntry=8; (c = zFile[iFile])!=0 && c!='.'; iFile++){
      if( sqlite3Isalpha(c) ){
        zAltEntry[iEntry++] = (char)sqlite3UpperToLower[(unsigned)c];
      }
    }
    memcpy(zAltEntry+iEntry, "_init", 6);
    zEntry = zAltEntry;
    xInit = (int(*)(sqlite3*,char**,const sqlite3_api_routines*))
                     sqlite3OsDlSym(pVfs, handle, zEntry);
  }

このように古いPHPだとバンドルされているSQLiteのエントリーポイントの取得方法が異なるために、エラーになるのです。

何故、Windowsではsqlite3_modspatialite_initというエントリーポイントを取得できないか

次にWindowsではどうして新しいPHPでもエントリーポイントを取得できないか説明します。
PHPはsqlite3.extension_dir で指定したフォルダ名と、loadExtensionで指定したファイル名を組み合わせて、sqlite3LoadExtensionを実行しています。

つまり、sqlite3LoadExtensionに渡されるzFileは次のようになります。

C:\tool\spatialite\mod_spatialite-4.2.0-win-x86\mod_spatialite.dll

UNIXの場合、パスの区切りが「/」なので適切にファイル名を抽出しますが、Windowsだと「\」なので抽出しません。この場合、エントリーポイントとして期待されるのが以下のようになってしまうのです。

ctoolspatialitemod_spatialitewinmod_spatialite

対応策

ようするにエントリーポイントを認識させればいいのです。
残念なことにPHPでは、以下のようなSQLを認めていません。

SELECT load_extension("mod_spatialite", "sqlite3_modspatialite_init")'

sqlite3.c
PHP_METHOD(sqlite3, loadExtension)
{
    php_sqlite3_db_object *db_obj;
    zval *object = getThis();
    char *extension, *lib_path, *extension_dir, *errtext = NULL;
    char fullpath[MAXPATHLEN];
    int extension_len, extension_dir_len;
    db_obj = (php_sqlite3_db_object *)zend_object_store_get_object(object TSRMLS_CC);

    SQLITE3_CHECK_INITIALIZED(db_obj, db_obj->initialised, SQLite3)

    if (FAILURE == zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &extension, &extension_len)) {
        return;
    }

#ifdef ZTS
    if ((strncmp(sapi_module.name, "cgi", 3) != 0) &&
        (strcmp(sapi_module.name, "cli") != 0) &&
        (strncmp(sapi_module.name, "embed", 5) != 0)
    ) {     php_sqlite3_error(db_obj, "Not supported in multithreaded Web servers");
        RETURN_FALSE;
    }
#endif

    if (!SQLITE3G(extension_dir)) {
        php_sqlite3_error(db_obj, "SQLite Extension are disabled");
        RETURN_FALSE;
    }

    if (extension_len == 0) {
        php_sqlite3_error(db_obj, "Empty string as an extension");
        RETURN_FALSE;
    }

    extension_dir = SQLITE3G(extension_dir);
    extension_dir_len = strlen(SQLITE3G(extension_dir));

    if (IS_SLASH(extension_dir[extension_dir_len-1])) {
        spprintf(&lib_path, 0, "%s%s", extension_dir, extension);
    } else {
        spprintf(&lib_path, 0, "%s%c%s", extension_dir, DEFAULT_SLASH, extension);
    }

    if (!VCWD_REALPATH(lib_path, fullpath)) {
        php_sqlite3_error(db_obj, "Unable to load extension at '%s'", lib_path);
        efree(lib_path);
        RETURN_FALSE;
    }

    efree(lib_path);

    if (strncmp(fullpath, extension_dir, extension_dir_len) != 0) {
        php_sqlite3_error(db_obj, "Unable to open extensions outside the defined directory");
        RETURN_FALSE;
    }

    /* Extension loading should only be enabled for when we attempt to load */
    sqlite3_enable_load_extension(db_obj->db, 1);
    if (sqlite3_load_extension(db_obj->db, fullpath, 0, &errtext) != SQLITE_OK) {
        php_sqlite3_error(db_obj, "%s", errtext);
        sqlite3_free(errtext);
        sqlite3_enable_load_extension(db_obj->db, 0);
        RETURN_FALSE;
    }
    sqlite3_enable_load_extension(db_obj->db, 0);

    RETURN_TRUE;
}

loadExtensionをするまえにsqlite3_enable_load_extensionで拡張モジュールを許可して、処理が終了したら不許可にしていることがわかります。
つまり、PHPの思想としてはSELECTでload_extensionはさせないというものになります。

これ以外に回避する方法は3つあります。

1つ mod_spatialiteのソースコードでエントリーポイント名を変更する
2つ mod_spatialite.dllをバイナリエディタで開いて、エントリーポイント名を改ざんする
3つ sqliteのコードで「\」も考慮するようにする。

以上の3つです。

mod_spatialiteのソースコードでエントリーポイント名を変更する

これに関しては未検証です。
ただ、エントリーポイントを修正するのは楽でも、mod_spatialiteをWindowsでビルドする環境を作るのはしんどいと思います。

mod_spatialite.dllをバイナリエディタで開いて、エントリーポイント名を改ざんする

spatialite_bin.png

spatialite_bin2.png

sqlite3_modspatialite_initという文字をバイナリエディタで検索して、sqlite3_extension_initに置き換えます。この際、足りないところには00で埋めます。削除したりすると、アドレスが変わるので動作しなくなります。

この方法で対応した場合、RTreeインデックスを使用できなくなることに目をつぶればPHP5.3でもmod_spatialiteを利用できます。

sqliteのコードで「\」も考慮するようにする。

そもそも論として、sqlite3.cを以下のように修正すれば「\」でもファイル名のみを抽出します。

sqlite3.c
  if( xInit==0 && zProc==0 ){
    int iFile, iEntry, c;
    int ncFile = sqlite3Strlen30(zFile);
    zAltEntry = sqlite3_malloc(ncFile+30);
    if( zAltEntry==0 ){
      sqlite3OsDlClose(pVfs, handle);
      return SQLITE_NOMEM;
    }
    memcpy(zAltEntry, "sqlite3_", 8);
    // Windows 対応
    //for(iFile=ncFile-1; iFile>=0 && zFile[iFile]!='/'; iFile--){}
    for(iFile=ncFile-1; iFile>=0 && (zFile[iFile]!='/' && zFile[iFile]!='\\'); iFile--){}

あとはPHPのソースコードをWindowsでビルドする方法ですが、以下を参考にすると良いでしょう。

PHP拡張モジュールを Windows でビルド
http://ngyuki.hatenablog.com/entry/20120625/p1

PHP を Windows でビルド
http://ngyuki.hatenablog.com/entry/20120701/p1

php_sqlite3.dllを作成するには次のようなconfigureでいけました。

configure --disable-all --enable-cli --enable-cgi  --with-sqlite3=shared
nmake

一応x86でPHP5.6のSQLiteをビルドしたものを以下に置きます。
http://needtec.sakura.ne.jp/release/php_sqlite3.zip

まとめ

このように、SpatialiteをPHPで使うにはかなり苦労が必要です。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4