diff --git a/.phpunit.result.cache b/.phpunit.result.cache index 7b1a906..dce0cae 100644 --- a/.phpunit.result.cache +++ b/.phpunit.result.cache @@ -1 +1 @@ -{"version":1,"defects":{"Wazza\\DomTranslate\\Tests\\Feature\\BladeTranslateTest::testGenericTranslate":7},"times":{"Wazza\\DomTranslate\\Tests\\Unit\\PhraseTest::testAssociateTranslationToPhrase":0.244,"Wazza\\DomTranslate\\Tests\\Unit\\PhraseTest::testAssociateMultipleTranslationsToPhrase":0.225,"Wazza\\DomTranslate\\Tests\\Unit\\PhraseTest::testDissociateTranslationFromPhrase":0.267,"Wazza\\DomTranslate\\Tests\\Unit\\PhraseTest::testDissociateAllTranslationsFromPhrase":0.253,"Wazza\\DomTranslate\\Tests\\Unit\\TranslationTest::testTranslationCreation":0.147,"Wazza\\DomTranslate\\Tests\\Feature\\BladeTranslateTest::testGenericTranslate":0.808}} \ No newline at end of file +{"version":1,"defects":{"Wazza\\DomTranslate\\Tests\\Feature\\BladeTranslateTest::testGenericTranslate":7},"times":{"Wazza\\DomTranslate\\Tests\\Unit\\PhraseTest::testAssociateTranslationToPhrase":0.105,"Wazza\\DomTranslate\\Tests\\Unit\\PhraseTest::testAssociateMultipleTranslationsToPhrase":0.073,"Wazza\\DomTranslate\\Tests\\Unit\\PhraseTest::testDissociateTranslationFromPhrase":0.089,"Wazza\\DomTranslate\\Tests\\Unit\\PhraseTest::testDissociateAllTranslationsFromPhrase":0.076,"Wazza\\DomTranslate\\Tests\\Unit\\TranslationTest::testTranslationCreation":0.072,"Wazza\\DomTranslate\\Tests\\Feature\\BladeTranslateTest::testGenericTranslate":1.866}} \ No newline at end of file diff --git a/README.md b/README.md index 3831ff3..3bfd1fe 100644 --- a/README.md +++ b/README.md @@ -2,28 +2,33 @@ GitHub issues GitHub stars GitHub license + GitHub version

-# domTranslate -A library that will use the built-in Laravel Derictive and provide automated translations to all your blade phrases or words. +# Laravel Translate Package -_Example: Write HTML static data in English and display it in a different language on run time (...in real-time)._ +A library that leverages Laravel Directives to provide automated translations for all your Blade phrases or words. + +_Example: Write HTML static data in English and display it in a different language in real-time._ ## Overview -The library contains 3 database tables (_domt_phrases_, _domt_translations_ and _domt_languages_) that are used to retrieve translations using an indexed hash. +The library uses three database tables (_domt_phrases_, _domt_translations_, and _domt_languages_) to manage translations efficiently. + +1. On page load, the system searches for a specific translation using the provided phrase in the `@transl8()` directive from the _domt_translations_ table. + > Laravel generally cache views, so if the content of the entire page didn't change, steps 1 - 4 will not fire as the cached view will simply load. +2. If the translation is found, it is returned and displayed on the page without making an API call. +3. If the translation is not found _(not translated yet)_, the Google Translate API (or another defined provider) is called to retrieve the new translation. +4. The newly translated text is then inserted into the database to avoid future API calls for the same phrase. -1. On page load, the system will search for a specific translation using the provided phrase (in the `@transl8()` directive) in the _domt_translations_ table. -2. If the translation was found, it will be returned and displayed on the page _(no API call was made)_. -3. If no transalation were found _(not previously translated)_, call the Google Translate API endpoint _(or any other Provider)_ to retrieve the correct translation. -4. Insert the newly translated text into the DB so that we don't have to call the API again for the given phrase _(step 1 above)_. +> Note: To ensure quick retrieval of translations, each phrase is hashed and stored in an indexed table column. All searches are performed against this indexed column for optimal performance. ## Installation -> PHP 8.0 is a minimum requirement for this project. +> PHP 8.0 is the minimum requirement for this project. -1. Follow the below steps to install the package +Follow these steps to install the package: ```bash composer require wazza/dom-translate @@ -32,16 +37,17 @@ php artisan vendor:publish --tag="dom-translate-migrations" php artisan migrate ``` -2. Add `DOM_TRANSLATE_GOOGLE_KEY={key value from Google}` to your _.env_ file and run... +Add `DOM_TRANSLATE_GOOGLE_KEY={your_google_api_key}` to your `.env` file and run: ```bash php artisan config:cache ``` -The example below of all the supported env keys that can be added with their current default values if not given. The `KEY` (i.e. `DOM_TRANSLATE_GOOGLE_KEY`) items are required. +Below are all the supported `.env` keys with their default values if not provided. The `KEY` (i.e., `DOM_TRANSLATE_GOOGLE_KEY`) is required. ``` DOM_TRANSLATE_USE_SESSION=true +DOM_TRANSLATE_USE_DATABASE=true DOM_TRANSLATE_LOG_LEVEL=3 DOM_TRANSLATE_LOG_INDICATOR=dom-translate DOM_TRANSLATE_PROVIDER=google @@ -53,101 +59,101 @@ DOM_TRANSLATE_LANG_SRC=en DOM_TRANSLATE_LANG_DEST=af ``` -> **Note:** If you don't have a [Google Cloud Platform](https://cloud.google.com/gcp) account yet, click on the link and sign up. Create a new Project and add the _Cloud Translation API_ to it. You can use [Insomnia](https://insomnia.rest/download) (image below) to test your API key. +- If `DOM_TRANSLATE_USE_SESSION` is `true`, translations will be saved in the session and used as the first point of retrieval. +- If no translations are found in the session, or if `DOM_TRANSLATE_USE_SESSION` is `false`, translations will be retrieved from the database, provided they have been previously stored there. +- If translations are still not found, or if both `DOM_TRANSLATE_USE_SESSION` and `DOM_TRANSLATE_USE_DATABASE` are `false`, translations will be sourced from a third-party translation service (e.g., Google Translate). +- Depending on whether `DOM_TRANSLATE_USE_SESSION` and `DOM_TRANSLATE_USE_DATABASE` are `true`, the retrieved translation will be saved to either the session or the database. +- We strongly recommend setting `DOM_TRANSLATE_USE_DATABASE` to `true` _(default is `true` if not specified in your .env)_ to ensure we don't make repeated API calls _(also it's slower calling the API verses db/session lookup)_. + +> **Note:** If you don't have a [Google Cloud Platform](https://cloud.google.com/gcp) account, sign up and create a new project. Add the _Cloud Translation API_ to it. You can use [Insomnia](https://insomnia.rest/download) to test your API key. - - insomnia - + insomnia -3. Done. Review any configuration file changes that you might want to change. The config file was published to the main config folder. +Review any configuration file changes that you might want to make. The config file is published to the main config folder. -> All done: Start your service again and update your Blade files with the @transl8 directive. Only new un-translated phrases will be translated via the API call. Any future requests, for the same phrase, will be retrieved from the database. +> You're all set! 😉 + +Restart your service and update your Blade files with the `@transl8` directive. Only new untranslated phrases will trigger an API call. Future requests for the same phrase will be retrieved from the database. ## HTML Blade Example -Find below a few examples of how to use the translate Blade directive in your HTML (Blade) files. +Here are a few examples of how to use the translate Blade directive in your HTML (Blade) files: ```blade
- {{-- Fully dependant on the source and destination language settings, only provide a phrase (this is the default way) --}} + {{-- Default usage: Only provide a phrase --}}

@transl8("I like this feature.")

- {{-- Overwrite the default (1) Destination language by including a second (destination) argument --}} -

@transl8("We need to test it in the staging environment.","de")

+ {{-- Specify a destination language --}} +

@transl8("We need to test it in the staging environment.", "de")

- {{-- Overwrite the default (1) Source and (2) Destination languages by including a second (destination) and third (source) argument --}} -

@transl8("Wie weet waar Willem Wouter woon?","en","af")

+ {{-- Specify both source and destination languages --}} +

@transl8("Wie weet waar Willem Wouter woon?", "en", "af")

- {{-- Use a Blade Language Specific directive for each language --}} + {{-- Language-specific directives --}}

@transl8fr("This phrase will be translated to French.")

@transl8de("This phrase will be translated to German.")

@transl8je("This phrase will be translated to Japanese.")

- {{-- ...you can update the Laravel AppServiceProvider register() method and add more of your own directives but ultimately the default @transl8() should be sufficient --}} - - {{-- ...and lastly, a phrase that will not be translated --}} + {{-- A phrase that will not be translated --}}

This phrase will not be translated.

``` -## Blade Directive Example: +## Blade Directive Example -The below 4 directives are available by default (`@transl8()` is the **main one**). - -You are welcome to add more directly in your Laravel _AppServiceProvider_ file _(under the register() method)_ +Four directives are available by default (`@transl8()` is the main one). You can add more in your Laravel _AppServiceProvider_ file (under the `register()` method). ```php -// (1) Register the DEFAULT Blade Directives - @transl8() -// With `transl8`, only the first argument is required (phrase). -// If you do not supply the destination or source languages, the default values will be sourced from the Config file. -// -- Format: transl8('Phrase','target','source') -// -- Example: transl8('This must be translated to French.','fr') +// Register the default Blade directive - @transl8() +// Only the phrase argument is required. Default source and destination languages are sourced from the config file. +// - Format: transl8('Phrase','target','source') +// - Example: transl8('This must be translated to French.','fr') Blade::directive('transl8', function ($string) { return \Wazza\DomTranslate\Controllers\TranslateController::phrase($string); }); -// (2) Register DIRECT (language specific) Blade directives, all from English (source) -// (2.1) French - @transl8fr('phrase') +// Register language-specific Blade directives +// French - @transl8fr('phrase') Blade::directive('transl8fr', function ($string) { return \Wazza\DomTranslate\Controllers\TranslateController::translate($string, "fr", "en"); }); -// (2.2) German - @transl8de('phrase') +// German - @transl8de('phrase') Blade::directive('transl8de', function ($string) { return \Wazza\DomTranslate\Controllers\TranslateController::translate($string, "de", "en"); }); -// (2.3) Japanese - @transl8je('phrase') +// Japanese - @transl8je('phrase') Blade::directive('transl8je', function ($string) { return \Wazza\DomTranslate\Controllers\TranslateController::translate($string, "je", "en"); }); -// (2.4) etc. You can create your own in Laravel's AppServiceProvider register() method, but ultimately the default @transl8() should be sufficient. ``` -## Outstanding Development (Backlog) +## Future Development (Backlog) -- Create an alternative Translation Engine/s. Google Translate is currently the only supported option via `Wazza\DomTranslate\Controllers\ApiTranslate\GoogleTranslate()`. Other possible options that can be added: 'NLP Translation', 'Microsoft Translator', etc. (Important: Add the Translation path to the config file - see below) +- Translations are not always perfect. Create a Phrase vs Translation admin section that will allow admin users to change (update) a translated phase with corrections. +- Create alternative translation engines. Currently, only Google Translate is supported via `Wazza\DomTranslate\Controllers\ApiTranslate\GoogleTranslate()`. Other options to consider include NLP Translation, Microsoft Translator, etc. ```php - // line 14 in 'wazza\dom-translate\config\dom_translate.php' - // 3rd party translation service providers. - 'api' => [ - 'provider' => env('DOM_TRANSLATE_PROVIDER', 'google'), - 'google' => [ - 'controller' => "Wazza\DomTranslate\Controllers\ApiTranslate\GoogleTranslate", - 'endpoint' => "https://www.googleapis.com/language/translate/v2", - 'action' => "POST", - 'key' => env('DOM_TRANSLATE_GOOGLE_KEY', null), // https://console.cloud.google.com/apis/credentials - ], - // @todo - for developers wanting to contribute: - // fork the project and add more translate providers here... (and their \ApiTranslate\Class implementing CloudTranslateInterface) - // ... thanks ;) +// Line 14 in 'wazza\dom-translate\config\dom_translate.php' +// Third-party translation service providers +'api' => [ + 'provider' => env('DOM_TRANSLATE_PROVIDER', 'google'), + 'google' => [ + 'controller' => "Wazza\DomTranslate\Controllers\ApiTranslate\GoogleTranslate", + 'endpoint' => "https://www.googleapis.com/language/translate/v2", + 'action' => "POST", + 'key' => env('DOM_TRANSLATE_GOOGLE_KEY', null), // https://console.cloud.google.com/apis/credentials ], + // To contribute, fork the project and add more translation providers here, implementing CloudTranslateInterface +], ``` -## Run local tests +## Running Local Tests -``` +Run the following command to execute tests: + +```bash .\vendor\bin\phpunit ``` -**Important:** For the final 2 assert Tests to work, you would have to add your own personal [Google Translate key](https://console.cloud.google.com/apis/credentials) -as DOM_TRANSLATE_GOOGLE_KEY=xxx in .env (at the time of writing there were free options available) +**Important:** For the final two assert tests to work, add your personal [Google Translate key](https://console.cloud.google.com/apis/credentials) as `DOM_TRANSLATE_GOOGLE_KEY=xxx` in your `.env` file (free options are available at the time of writing), or directly in the `phpunit.xml` file under `DOM_TRANSLATE_GOOGLE_KEY`. diff --git a/config/dom_translate.php b/config/dom_translate.php index e2f3f6f..b8f1a4b 100644 --- a/config/dom_translate.php +++ b/config/dom_translate.php @@ -5,7 +5,13 @@ // This feature adds an additional layer above the normal DB storing functionality, // enabling the avoidance of unnecessary DB calls by retrieving translations from the session when available. // ------------------------------------------------------------ - 'use_session' => env('DOM_TRANSLATE_USE_SESSION', true), + 'use_session' => env('DOM_TRANSLATE_USE_SESSION', false), + + // If set to true, 'phrase' => 'translation' pairs will be stored in the database. + // This feature enables the storage of translations in the database, allowing for quick retrieval of translations. + // By default this should be set to `true` to ensure that we don't make repeated API calls. + // ------------------------------------------------------------ + 'use_database' => env('DOM_TRANSLATE_USE_DATABASE', true), // Determines the level of logging. For production environments, we recommend using either 0 or 1. // `level`: 0=None; 1=High-Level; 2=Mid-Level or 3=Low-Level diff --git a/src/Controllers/TranslateController.php b/src/Controllers/TranslateController.php index 4b9ed1b..2b0f1d7 100644 --- a/src/Controllers/TranslateController.php +++ b/src/Controllers/TranslateController.php @@ -9,6 +9,7 @@ use Wazza\DomTranslate\Language; use Wazza\DomTranslate\Translation; use Wazza\DomTranslate\Helpers\phraseHelper; +use Wazza\DomTranslate\Contracts\CloudTranslateInterface; use Exception; class TranslateController extends BaseController @@ -47,89 +48,114 @@ public static function phrase(?string $string = null) */ public static function translate(?string $srcPhrase = null, ?string $destCode = null, ?string $srcCode = null) { + LogController::log('notice', 1, '----- Translation request start -----'); LogController::log('notice', 1, 'New phrase to translate.'); // high-level = 1 // sanitise the phrase $srcPhrase = phraseHelper::sanitise($srcPhrase); - LogController::log('notice', 3, 'Phrase Sanitised: ' . $srcPhrase); // low-level = 3 + LogController::log('notice', 3, 'Phrase Sanitised ............... : ' . $srcPhrase); // low-level = 3 // hash the phrase $srcHash = phraseHelper::hash($srcPhrase); - LogController::log('notice', 3, 'Phrase Hashed: ' . $srcHash); + LogController::log('notice', 3, 'Phrase Hashed .................. : ' . $srcHash); // do we have a destination language defined $destCode = phraseHelper::prepDestLanguage($destCode); - LogController::log('notice', 2, 'Destination language code set as: ' . $destCode); + LogController::log('notice', 2, 'Destination language code set as : ' . $destCode); // do we have a source language defined $srcCode = phraseHelper::prepSrcLanguage($srcCode); - LogController::log('notice', 2, 'Source language code set as: ' . $srcCode); + LogController::log('notice', 2, 'Source language code set as .... : ' . $srcCode); // ok, ready to rock-and-roll... try { + // ------------------------------------------------------------ // (1) First search the Session for the unique hash (ideal scenario) - if enabled in config if (config('dom_translate.use_session', false) === true) { + LogController::log('notice', 3, 'Use Session is enabled. Searching for translation in Session...'); + if (session()->has($srcHash . $srcCode . $destCode)) { // session translation located - LogController::log('notice', 1, 'Translation located in Session. Return translation...'); + LogController::log('notice', 1, 'Translation located in Session. Returning translation...'); return session()->get($srcHash . $srcCode . $destCode); } + } else { + LogController::log('notice', 3, 'Use Session is disabled. Skipping Session search...'); } + // ------------------------------------------------------------ // (2) Search for the direct Translation in the DB using the Phrase HASH (ok scenario as well) - $translation = Translation::select('value') - ->whereHas('language', function ($query) use ($destCode) { - $query->where('code', $destCode); - })->whereHas('phrase', function ($queryl1) use ($srcHash, $srcCode) { - $queryl1->where('hash', $srcHash); - $queryl1->whereHas('language', function ($queryl2) use ($srcCode) { - $queryl2->where('code', $srcCode); - }); - })->first(); + if (config('dom_translate.use_database', true) === true) { + LogController::log('notice', 3, 'Use Database is enabled. Searching for translation in DB...'); + + // try to locate the translation in the DB + $translation = Translation::select('value') + ->whereHas('language', function ($query) use ($destCode) { + $query->where('code', $destCode); + })->whereHas('phrase', function ($queryl1) use ($srcHash, $srcCode) { + $queryl1->where('hash', $srcHash); + $queryl1->whereHas('language', function ($queryl2) use ($srcCode) { + $queryl2->where('code', $srcCode); + }); + })->first(); + + // did we locate a direct translation? + if (!is_null($translation)) { + // yes we did - awesome... + LogController::log('notice', 1, 'Translation located in DB. Return translation...'); + + // save to session so that future request (for this session) can be returned at step 1 above + if (config('dom_translate.use_session', false) === true && !session()->has($srcHash . $srcCode . $destCode)) { + LogController::log('notice', 2, 'DB Located translation saved to Session.'); + session()->put($srcHash . $srcCode . $destCode, $translation->value); + } + + // ...and return. + return $translation->value; + } + LogController::log('notice', 1, 'Translation NOT located in DB. Continue...'); + } else { + LogController::log('notice', 3, 'Use Database is disabled. Skipping DB search...'); + } - // did we locate a direct translation? - if (!is_null($translation)) { - // yes we did - awesome... - LogController::log('notice', 1, 'Translation located in DB. Return translation...'); + // ------------------------------------------------------------ + // We could not find a direct translation in the DB nor Session... We need to call the Cloud API + // ------------------------------------------------------------ - // save to session so that future request (for this session) can be returned at step 1 above - if (config('dom_translate.use_session', false) === true) { - session()->put($srcHash . $srcCode . $destCode, $translation->value); - } + // (3) We need to first locate the Phrase in the DB (if enabled in config), and insert it if not found + $phrase = null; + if (config('dom_translate.use_database', true) === true) { + // first see if we have the correct Phrase + $phrase = Phrase::where('hash', $srcHash)->whereHas('language', function ($q) use ($srcCode) { + $q->where('code', $srcCode); + })->first(); - // ...and return. - return $translation->value; - } - LogController::log('notice', 1, 'Translation NOT located in DB. Continue...'); - - // (3) We could not find a direct translation in the DB... We need to call the Cloud API - // (3.1) ... first see if we have the correct Phrase - $phrase = Phrase::where('hash', $srcHash)->whereHas('language', function ($q) use ($srcCode) { - $q->where('code', $srcCode); - })->first(); - - // if no phrase were located, insert a new record - if (is_null($phrase)) { - LogController::log('notice', 1, 'Could not locate the Phrase in the DB, we would need to insert it.'); - - // find the source language id - $languageSrc = Language::select('id')->where('code', $srcCode)->first(); - if (is_null($languageSrc)) { - throw new Exception('Phrase could not be inserted into DB because the source language (code: ' . $srcCode . ') could not be loaded.'); + // if no phrase were located, insert a new record + if (is_null($phrase)) { + LogController::log('notice', 1, 'Could not locate the Phrase in the DB, we would need to insert it.'); + + // find the source language id + $languageSrc = Language::select('id')->where('code', $srcCode)->first(); + if (is_null($languageSrc)) { + throw new Exception('Phrase could not be inserted into DB because the source language (code: ' . $srcCode . ') could not be loaded.'); + } + + // insert the new phrase into the DB + $phrase = new Phrase(); + $phrase->language_id = $languageSrc->id; + $phrase->hash = $srcHash; + $phrase->value = $srcPhrase; + $phrase->save(); + LogController::log('notice', 1, 'New Phrase saved in DB as ID - ' . $phrase->id); + } else { + LogController::log('notice', 1, 'Phrase located under ID - ' . $phrase->id); } - - // insert the new phrase into the DB - $phrase = new Phrase(); - $phrase->language_id = $languageSrc->id; - $phrase->hash = $srcHash; - $phrase->value = $srcPhrase; - $phrase->save(); - LogController::log('notice', 1, 'New Phrase saved in DB as ID - ' . $phrase->id); } else { - LogController::log('notice', 1, 'Phrase located under ID - ' . $phrase->id); + LogController::log('notice', 3, 'Use Database is disabled. Skipping Phrase check and insertion...'); } - // (3.2) ...ok, the phrase is in the DB. Let's get the correct Translation and insert it into the DB against the phrase + // ------------------------------------------------------------ + // (4) We need to call the Cloud API to get the translation LogController::log('notice', 1, 'Call API for Translation.'); $defaultProvider = config('dom_translate.api.provider'); @@ -139,33 +165,44 @@ public static function translate(?string $srcPhrase = null, ?string $destCode = LogController::log('notice', 2, 'Provider Controller - ' . $providerController); // bind the Provider Translation Controller with the `Cloud Translate Interface` - App::bind(Wazza\DomTranslate\Contracts\CloudTranslateInterface::class, $providerController); + App::bind(CloudTranslateInterface::class, $providerController); LogController::log('notice', 3, 'Provider Controller ' . $providerController . ' binded to the CloudTranslateInterface Class.'); // initiate the cloud translate request on the binded provider class - $translatedString = App::make(Wazza\DomTranslate\Contracts\CloudTranslateInterface::class)->cloudTranslate($srcPhrase, $destCode, $srcCode); + $translatedString = App::make(CloudTranslateInterface::class)->cloudTranslate($srcPhrase, $destCode, $srcCode); - // (4) insert translated text into db - // (4.1) find the destination language id - $languageDest = Language::select('id')->where('code', $destCode)->first(); - if (is_null($languageDest)) { - throw new Exception('Translation could not be inserted into DB because the destincation language could not be loaded.'); - } - LogController::log('notice', 1, 'Destination Language (code: ' . $languageDest->id . ') located at ID - ' . $languageDest->id); + // ------------------------------------------------------------ + // (5) insert translated text into db (if enabled in config) + if (!is_null($phrase) && config('dom_translate.use_database', true) === true) { + LogController::log('notice', 3, 'Use Database is enabled. Inserting translation into DB...'); - // (4.2) insert new tranlation into db - $translation = new Translation(); - $translation->language_id = $languageDest->id; - $translation->phrase_id = $phrase->id; - $translation->value = $translatedString; - $translation->save(); - LogController::log('notice', 1, 'New Translation inserted into DB at ID - ' . $translation->id); + // (5.1) find the destination language id + $languageDest = Language::select('id')->where('code', $destCode)->first(); + if (is_null($languageDest)) { + throw new Exception('Translation could not be inserted into DB because the destincation language could not be loaded.'); + } + LogController::log('notice', 1, 'Destination Language (code: ' . $languageDest->id . ') located at ID - ' . $languageDest->id); + + // (5.2) insert new translation into db + $translation = new Translation(); + $translation->language_id = $languageDest->id; + $translation->phrase_id = $phrase->id; + $translation->value = $translatedString; + $translation->save(); + LogController::log('notice', 1, 'New Translation inserted into DB at ID - ' . $translation->id); + } else { + LogController::log('notice', 3, 'Use Database is disabled (or source phrase object is null). Skipping DB insertion...'); + } - // save to session so that future request (for this session) can be returned at step 1 above + // (6) save to session so that future request (for this session) can be returned at step 1 above if (config('dom_translate.use_session', false) === true) { + LogController::log('notice', 3, 'Use Session is enabled. Inserting translation into Session...'); session()->put($srcHash . $srcCode . $destCode, $translatedString); + } else { + LogController::log('notice', 3, 'Use Session is disabled. Skipping Session insertion...'); } + // ------------------------------------------------------------ // return the newly translated text return $translatedString; } catch (Exception $e) { diff --git a/tests/testdb.sqlite b/tests/testdb.sqlite index 8346cfa..55d0d6e 100644 Binary files a/tests/testdb.sqlite and b/tests/testdb.sqlite differ