├── .gitattributes ├── .gitignore ├── .editorconfig ├── src ├── FFI │ ├── PDO │ │ ├── SQLite │ │ │ ├── pdo_sqlite.h │ │ │ └── DriverDataTraverser.php │ │ ├── pdo.h │ │ └── DriverDataResolver.php │ ├── PHPSQLite3 │ │ ├── php_sqlite3_structs.h │ │ └── DbHandleResolver.php │ └── SQLite3 │ │ ├── ConnectionWrapper.php │ │ └── sqlite3.h ├── WrappedConnection.php └── Facade.php ├── test └── Integration │ ├── PHPSQLite3SupportTest.php │ ├── GetDatabaseFilenameTest.php │ └── ExtensionLoadingTest.php ├── composer.json ├── .github └── workflows │ └── ci.yml ├── LICENSE └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /.idea/ 3 | /.phpunit.result.cache 4 | /composer.lock 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | 9 | [*.php] 10 | indent_style = space 11 | indent_size = 4 12 | -------------------------------------------------------------------------------- /src/FFI/PDO/SQLite/pdo_sqlite.h: -------------------------------------------------------------------------------- 1 | /* Adapted from https://github.com/php/php-src/blob/cfc704ea83c56970a72756f7d4fe464885445b5e/ext/pdo_sqlite/php_pdo_sqlite_int.h#L55 */ 2 | struct pdo_sqlite_db_handle { 3 | /* replaced sqlite3* by void* */ 4 | void *db; 5 | /* omitted rest of struct */ 6 | }; 7 | -------------------------------------------------------------------------------- /src/FFI/PHPSQLite3/php_sqlite3_structs.h: -------------------------------------------------------------------------------- 1 | /* Adapted from https://github.com/php/php-src/blob/2a76e3a4571a7e31905a569580682e68cc003abb/ext/sqlite3/php_sqlite3_structs.h#L71 */ 2 | struct _php_sqlite3_db_object { 3 | int initialised; 4 | /* replaced sqlite3* by void* */ 5 | void *db; 6 | /* omitted rest of struct */ 7 | }; 8 | -------------------------------------------------------------------------------- /src/FFI/PDO/pdo.h: -------------------------------------------------------------------------------- 1 | /* From https://github.com/php/php-src/blob/d1764ca33018f1f2e4a05926c879c67ad4aa8da5/ext/pdo/php_pdo_driver.h#L432 */ 2 | struct _pdo_dbh_t { 3 | /* replaced pdo_dbh_methods* by void* */ 4 | const void *methods; 5 | void *driver_data; 6 | /* omitted rest of struct */ 7 | }; 8 | 9 | /* From https://github.com/php/php-src/blob/d1764ca33018f1f2e4a05926c879c67ad4aa8da5/ext/pdo/php_pdo_driver.h#L510 */ 10 | struct _pdo_dbh_object_t { 11 | /* had to insert struct keyword here */ 12 | struct _pdo_dbh_t *inner; 13 | /* omitted `zend_object std` */ 14 | }; 15 | -------------------------------------------------------------------------------- /test/Integration/PHPSQLite3SupportTest.php: -------------------------------------------------------------------------------- 1 | assertSame($temp_file, $wrapped_connection->getDatabaseFilename()); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/FFI/PDO/SQLite/DriverDataTraverser.php: -------------------------------------------------------------------------------- 1 | pdo_sqlite_ffi = \FFI::cdef(file_get_contents(__DIR__ . "/pdo_sqlite.h"), "pdo_sqlite.so"); 9 | } 10 | 11 | public function getSQLite3Pointer(\FFI\CData $driver_data_void_pointer): \FFI\CData { 12 | $pdo_sqlite_db_handle_pointer = $this->pdo_sqlite_ffi->cast("struct pdo_sqlite_db_handle*", $driver_data_void_pointer); 13 | return $pdo_sqlite_db_handle_pointer[0]->db; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/FFI/SQLite3/ConnectionWrapper.php: -------------------------------------------------------------------------------- 1 | sqlite3_ffi = \FFI::cdef(file_get_contents(__DIR__ . "/sqlite3.h"), "sqlite3.so"); 11 | } 12 | 13 | public function wrapConnection(\FFI\CData $sqlite3_void_pointer): WrappedConnection { 14 | $sqlite3_pointer = $this->sqlite3_ffi->cast("struct sqlite3*", $sqlite3_void_pointer); 15 | return new WrappedConnection($this->sqlite3_ffi, $sqlite3_pointer); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "moxio/sqlite-extended-api", 3 | "type": "library", 4 | "description": "Exposes SQLite APIs that are otherwise not available in PHP", 5 | "license": "MIT", 6 | "keywords" : [ "sqlite", "sqlite3", "pdo", "ffi", "z-engine" ], 7 | "authors": [ 8 | { 9 | "name": "Moxio", 10 | "homepage": "https://www.moxio.com" 11 | } 12 | ], 13 | "autoload": { 14 | "psr-4": { 15 | "Moxio\\SQLiteExtendedAPI\\": "src/" 16 | } 17 | }, 18 | "autoload-dev": { 19 | "psr-4": { 20 | "Moxio\\SQLiteExtendedAPI\\Test\\": "test/" 21 | } 22 | }, 23 | "require": { 24 | "php": "^7.4 || ^8.0", 25 | "ext-FFI": "*", 26 | "lisachenko/z-engine": "^0.8.0 || ^0.9.1" 27 | }, 28 | "require-dev": { 29 | "phpunit/phpunit": "^9.1" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | php-version: [ '7.4', '8.0' ] 17 | 18 | steps: 19 | - name: Checkout code 20 | uses: actions/checkout@v2 21 | 22 | - name: Setup PHP 23 | uses: shivammathur/setup-php@v2 24 | with: 25 | php-version: ${{ matrix.php-version }} 26 | 27 | - name: Validate composer.json 28 | run: composer validate 29 | 30 | - name: Install dependencies 31 | run: composer install --prefer-dist --no-progress --no-suggest 32 | 33 | - name: Run test suite 34 | run: php vendor/bin/phpunit test 35 | -------------------------------------------------------------------------------- /test/Integration/GetDatabaseFilenameTest.php: -------------------------------------------------------------------------------- 1 | assertSame($temp_file, $wrapped_connection->getDatabaseFilename()); 14 | } 15 | 16 | public function testReturnsEmptyStringForInMemoryConnection() { 17 | $pdo = new \PDO('sqlite::memory:'); 18 | $wrapped_connection = Facade::wrapPDO($pdo); 19 | $this->assertSame("", $wrapped_connection->getDatabaseFilename()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/FFI/SQLite3/sqlite3.h: -------------------------------------------------------------------------------- 1 | /* From https://github.com/sqlite/sqlite/blob/278b0517d88d4150830a4ee2c628a55da40d186d/src/sqlite.h.in#L249 */ 2 | typedef struct sqlite3 sqlite3; 3 | 4 | /* From https://github.com/sqlite/sqlite/blob/278b0517d88d4150830a4ee2c628a55da40d186d/src/sqlite.h.in#L1595 */ 5 | int sqlite3_db_config(sqlite3*, int op, ...); 6 | 7 | /* From https://github.com/sqlite/sqlite/blob/278b0517d88d4150830a4ee2c628a55da40d186d/src/sqlite.h.in#L6173 */ 8 | const char *sqlite3_db_filename(sqlite3 *db, const char *zDbName); 9 | 10 | /* From https://github.com/sqlite/sqlite/blob/278b0517d88d4150830a4ee2c628a55da40d186d/src/sqlite.h.in#L6581 */ 11 | int sqlite3_load_extension( 12 | sqlite3 *db, /* Load the extension into this database connection */ 13 | const char *zFile, /* Name of the shared library containing extension */ 14 | const char *zProc, /* Entry point. Derived from zFile if 0 */ 15 | char **pzErrMsg /* Put error message here if not 0 */ 16 | ); 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Moxio 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/FFI/PDO/DriverDataResolver.php: -------------------------------------------------------------------------------- 1 | pdo_ffi = \FFI::cdef(file_get_contents(__DIR__ . "/pdo.h"), "pdo.so"); 16 | } 17 | 18 | public function getDriverDataPointer(\PDO $pdo): \FFI\CData { 19 | $pdo_refl_value = new ReflectionValue($pdo); 20 | $pdo_obj_pointer = $pdo_refl_value->getRawObject(); 21 | $offset = $pdo_obj_pointer->handlers->offset; 22 | 23 | // Following https://github.com/php/php-src/blob/d1764ca33018f1f2e4a05926c879c67ad4aa8da5/ext/pdo/php_pdo_driver.h#L520 24 | $pdo_dbh_object_char_pointer = $this->pdo_ffi->cast("char*", $pdo_obj_pointer) - $offset; 25 | $pdo_dbh_object_pointer = $this->pdo_ffi->cast("struct _pdo_dbh_object_t*", $pdo_dbh_object_char_pointer); 26 | $pdo_dbh_pointer = $pdo_dbh_object_pointer[0]->inner; 27 | 28 | return $pdo_dbh_pointer[0]->driver_data; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/FFI/PHPSQLite3/DbHandleResolver.php: -------------------------------------------------------------------------------- 1 | php_sqlite3_ffi = \FFI::cdef(file_get_contents(__DIR__ . "/php_sqlite3_structs.h"), "sqlite3.so"); 16 | } 17 | 18 | public function getSQLite3Pointer(\SQLite3 $php_sqlite3): \FFI\CData { 19 | $php_sqlite3_refl_value = new ReflectionValue($php_sqlite3); 20 | $php_sqlite3_obj_pointer = $php_sqlite3_refl_value->getRawObject(); 21 | $offset = $php_sqlite3_obj_pointer->handlers->offset; 22 | 23 | // Following https://github.com/php/php-src/blob/2a76e3a4571a7e31905a569580682e68cc003abb/ext/sqlite3/php_sqlite3_structs.h#L83 24 | $php_sqlite3_db_object_char_pointer = $this->php_sqlite3_ffi->cast("char*", $php_sqlite3_obj_pointer) - $offset; 25 | $php_sqlite3_db_object_pointer = $this->php_sqlite3_ffi->cast("struct _php_sqlite3_db_object*", $php_sqlite3_db_object_char_pointer); 26 | return $php_sqlite3_db_object_pointer[0]->db; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/WrappedConnection.php: -------------------------------------------------------------------------------- 1 | sqlite3_ffi = $sqlite3_ffi; 18 | $this->sqlite3_pointer = $sqlite3_pointer; 19 | } 20 | 21 | public function getDatabaseFilename(): string { 22 | return $this->sqlite3_ffi->sqlite3_db_filename($this->sqlite3_pointer, "main"); 23 | } 24 | 25 | public function loadExtension(string $shared_library): bool { 26 | $this->sqlite3_ffi->sqlite3_db_config($this->sqlite3_pointer, self::SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION, 1, null); 27 | $result_code = $this->sqlite3_ffi->sqlite3_load_extension($this->sqlite3_pointer, $shared_library, null, null); 28 | 29 | return $result_code === self::SQLITE_OK; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/Integration/ExtensionLoadingTest.php: -------------------------------------------------------------------------------- 1 | pdo = new \PDO('sqlite::memory:'); 14 | $this->wrapped_connection = Facade::wrapPDO($this->pdo); 15 | } 16 | 17 | private const EXTENSION = 'mod_spatialite.so'; 18 | private const EXTENSION_VERIFICATION_QUERY = "SELECT ST_AsText(ST_GeomFromText('POINT(155000 463000)'))"; 19 | 20 | public function testLoadExtensionLoadsAnSQLiteExtension() { 21 | $extension_dir = ini_get('sqlite3.extension_dir') ?: '/usr/lib/x86_64-linux-gnu'; 22 | $extension_file = $extension_dir . '/' . self::EXTENSION; 23 | if (!file_exists($extension_file)) { 24 | $this->markTestSkipped(sprintf("SQLite extension file '%s' needed for test not found", self::EXTENSION)); 25 | } 26 | 27 | $this->assertTrue($this->wrapped_connection->loadExtension(self::EXTENSION)); 28 | $this->assertNotFalse($this->pdo->query(self::EXTENSION_VERIFICATION_QUERY)); 29 | } 30 | 31 | public function testLoadExtensionReturnsFalseIfExtensionCouldNotBeLoaded() { 32 | $extension_dir = ini_get('sqlite3.extension_dir') ?: '/usr/lib/x86_64-linux-gnu'; 33 | $extension_file = $extension_dir . '/mod_does_not_exist.so'; 34 | 35 | $this->assertFalse($this->wrapped_connection->loadExtension($extension_file)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Facade.php: -------------------------------------------------------------------------------- 1 | getDriverDataPointer($pdo); 24 | 25 | if (isset(self::$pdo_sqlite_driver_data_traverser) === false) { 26 | self::$pdo_sqlite_driver_data_traverser = new PDOSQLiteDriverDataTraverser(); 27 | } 28 | $sqlite3_void_pointer = self::$pdo_sqlite_driver_data_traverser->getSQLite3Pointer($pdo_driver_data_void_pointer); 29 | 30 | if (isset(self::$sqlite3_connection_wrapper) === false) { 31 | self::$sqlite3_connection_wrapper = new SQLite3ConnectionWrapper(); 32 | } 33 | 34 | return self::$sqlite3_connection_wrapper->wrapConnection($sqlite3_void_pointer); 35 | } 36 | 37 | public static function wrapSQLite3(\SQLite3 $sqlite3): WrappedConnection { 38 | if (isset(self::$php_sqlite3_db_handle_resolver) === false) { 39 | self::$php_sqlite3_db_handle_resolver = new PHPSQLite3DbHandleResolver(); 40 | } 41 | $sqlite3_void_pointer = self::$php_sqlite3_db_handle_resolver->getSQLite3Pointer($sqlite3); 42 | 43 | if (isset(self::$sqlite3_connection_wrapper) === false) { 44 | self::$sqlite3_connection_wrapper = new SQLite3ConnectionWrapper(); 45 | } 46 | 47 | return self::$sqlite3_connection_wrapper->wrapConnection($sqlite3_void_pointer); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Latest Stable Version](https://poser.pugx.org/moxio/sqlite-extended-api/v/stable)](https://packagist.org/packages/moxio/sqlite-extended-api) 2 | [![Buy us a tree](https://img.shields.io/badge/Treeware-%F0%9F%8C%B3-lightgreen)](https://plant.treeware.earth/Moxio/sqlite-extended-api) 3 | 4 | moxio/sqlite-extended-api 5 | ========================= 6 | 7 | Exposes SQLite APIs that are otherwise not available in PHP. You can connect 8 | to an SQLite database as you normally would using PHP's `PDO` extension, then 9 | use this library to call SQLite API methods that `PDO` does not offer (e.g. 10 | loading extensions). 11 | 12 | **Warning**: under the hood, this library makes use of [Z-Engine](https://github.com/lisachenko/z-engine), 13 | which proclaims itself not ready for production until version 1.0.0. Use it at 14 | your own risk. 15 | 16 | Requirements 17 | ------------ 18 | This library requires PHP version 7.4 or higher with the FFI extension enabled. 19 | It only works with x64 non-thread-safe builds of PHP. 20 | 21 | Installation 22 | ------------ 23 | Install as a dependency using composer: 24 | ``` 25 | $ composer require moxio/sqlite-extended-api 26 | ``` 27 | 28 | Usage 29 | ----- 30 | If you have an existing `PDO` connection to an SQLite database, you can use the 31 | `wrapPDO()` static method on the `Facade` class to obtain access to extra SQLite 32 | APIs: 33 | 34 | ```php 35 | loadExtension('mod_spatialite.so'); 46 | ``` 47 | 48 | See the next section for methods available on the wrapped connection. 49 | 50 | Exposed APIs 51 | ------------- 52 | Below is a short overview; see [`WrappedConnection`](src/WrappedConnection.php) 53 | for details. 54 | 55 | ### Loading SQLite extensions 56 | Load additional SQLite extension libraries using `loadExtension($shared_library)`: 57 | ```php 58 | $wrapped_connection->loadExtension('mod_spatialite.so'); 59 | ``` 60 | This corresponds to the [`loadExtension`](https://www.php.net/manual/en/sqlite3.loadextension.php) 61 | method in PHP's SQLite3 extension, or [`sqlite3_load_extension](https://sqlite.org/c3ref/load_extension.html) 62 | in the SQLite C interface. Returns `true` if the extension was successfully loaded, 63 | false if it was not. 64 | 65 | ### Obtaining the database filename 66 | To obtain the full disk path of the database connected to, use `getDatabaseFilename()`: 67 | ```php 68 | var_dump($wrapped_connection->getDatabaseFilename()); 69 | ``` 70 | For an in-memory database, this returns an empty string. 71 | 72 | How does this work? 73 | ------------------- 74 | In short: we use the awesome [Z-Engine](https://github.com/lisachenko/z-engine) 75 | project by [Alexander Lisachenko](https://twitter.com/lisachenko) and PHP's 76 | [Foreign Function Interface (FFI)](https://www.php.net/manual/en/book.ffi.php) 77 | to resolve your PHP variable to the raw connection pointer for the SQLite C API, 78 | then call that C API using FFI. 79 | 80 | More details can be found in [this blog post](https://www.moxio.com/blog/47/how-to-load-an-sqlite-extension-in-pdo). 81 | 82 | Versioning 83 | ---------- 84 | This project adheres to [Semantic Versioning](http://semver.org/). 85 | 86 | Contributing 87 | ------------ 88 | Contributions to this project are more than welcome. If there are other SQLite 89 | APIs that you would like to be able to use in PHP, feel free to send a PR or 90 | to file a feature request. 91 | 92 | License 93 | ------- 94 | This project is released under the MIT license. 95 | 96 | Treeware 97 | -------- 98 | This package is [Treeware](https://treeware.earth/). If you use it in production, 99 | then we'd appreciate it if you [**buy the world a tree**](https://plant.treeware.earth/Moxio/sqlite-extended-api) 100 | to thank us for our work. By contributing to the Treeware forest you'll be creating 101 | employment for local families and restoring wildlife habitats. 102 | 103 | --- 104 | Made with love, coffee and fun by the [Moxio](https://www.moxio.com) team from 105 | Delft, The Netherlands. Interested in joining our awesome team? Check out our 106 | [vacancies](https://werkenbij.moxio.com/) (in Dutch). 107 | --------------------------------------------------------------------------------